Compare commits

...

89 Commits

Author SHA1 Message Date
Abdullah Atta
4906c4f293 core: update @streetwriters/showdown 2023-12-21 11:15:57 +05:00
Abdullah Atta
02e44d9502 editor: add support for simple checklists 2023-12-21 11:08:54 +05:00
Ammar Ahmed
d9d82232c0 editor: implement simple checklists 2023-12-08 12:21:10 +05:00
Ammar Ahmed
ed81319d2c mobile: use index based grouping 2023-12-08 10:13:43 +05:00
Abdullah Atta
d65917c85b web: changes to use new grouping api 2023-12-05 15:34:55 +05:00
Abdullah Atta
528aefca46 core: implement new high perf indexed based grouping 2023-12-05 15:34:55 +05:00
Ammar Ahmed
356142bb22 mobile: fix share extension in release 2023-12-04 12:32:40 +05:00
Abdullah Atta
dad1742e80 web: fix router types 2023-12-04 11:35:42 +05:00
Abdullah Atta
c68ed0dc52 web: enable vite hot reloading 2023-12-04 11:35:29 +05:00
Abdullah Atta
978afa0899 web: fix minor build issues 2023-11-30 15:22:02 +05:00
Abdullah Atta
14d57f200b web: migrate everything to react@18 2023-11-30 15:02:37 +05:00
Abdullah Atta
2f4f351fb6 web: fix notebook tests 2023-11-30 13:04:37 +05:00
Ammar Ahmed
1344ba3821 mobile: share extension cleanup 2023-11-30 12:20:22 +05:00
Ammar Ahmed
21f9930cd0 mobile: cleanup 2023-11-30 11:21:57 +05:00
Ammar Ahmed
0789f0c075 mobile: migrate share extension 2023-11-30 11:21:57 +05:00
Ammar Ahmed
8e1d7e097c mobile: fix navigation 2023-11-30 11:21:57 +05:00
Abdullah Atta
f3a977c070 web: send noteId when decrypting real-time locked content 2023-11-28 13:56:08 +05:00
Abdullah Atta
c6fad05c56 web: fix tag tests 2023-11-28 13:56:08 +05:00
Ammar Ahmed
72a99ff751 mobile: pass tests 2023-11-27 16:33:09 +05:00
Abdullah Atta
1fcc4aa0b4 web: fix html export snapshot 2023-11-27 10:42:30 +05:00
Abdullah Atta
3a97cdeb3d core: fix md with frontmatter export 2023-11-27 10:36:16 +05:00
Ammar Ahmed
7cf6441b9c core: fix ts error 2023-11-27 10:03:24 +05:00
Ammar Ahmed
cdd3075e4f mobile: fix webpack build 2023-11-27 10:03:24 +05:00
Ammar Ahmed
162d16b2c7 mobile: push changes 2023-11-27 10:03:24 +05:00
Abdullah Atta
8e0559ca85 web: fix notes tests 2023-11-27 10:03:23 +05:00
Abdullah Atta
e92e54684e core: prevent duplicate colors based on color code 2023-11-27 10:03:23 +05:00
Abdullah Atta
9321aa1732 core: fix error on locking notes 2023-11-27 10:03:23 +05:00
Ammar Ahmed
5daeda890c mobile: push changes 2023-11-27 10:03:23 +05:00
Abdullah Atta
f6dbf253fa web: reset search results on navigation 2023-11-27 10:03:23 +05:00
Abdullah Atta
acf993f06e web: reset prepared sql stmt on error 2023-11-27 10:03:23 +05:00
Abdullah Atta
4b4a388f5f web: reuse search query 2023-11-27 10:03:23 +05:00
Abdullah Atta
3fd216bfd4 core: use porter trigram tokenizer for fts 2023-11-27 10:03:23 +05:00
Abdullah Atta
a2bbde30e6 core: properly escape fts query 2023-11-27 10:03:23 +05:00
Abdullah Atta
2e4035cd7b web: add support for assigning tags to notes 2023-11-27 10:03:23 +05:00
Abdullah Atta
e705793e0a core: fix "type" does not exist 2023-11-27 10:03:23 +05:00
Abdullah Atta
37c0107660 core: fix build error 2023-11-27 10:03:22 +05:00
Abdullah Atta
5bfcf93713 core: skip attachment if it already exists on backup restore 2023-11-27 10:03:22 +05:00
Ammar Ahmed
920ff08f16 mobile: migrate to sqlite 2023-11-27 10:03:22 +05:00
Abdullah Atta
6b85150acb web: fix note editor properties 2023-11-27 10:01:20 +05:00
Abdullah Atta
0e243fbeb4 common: type is now optional in getFormattedDate 2023-11-27 10:01:20 +05:00
Abdullah Atta
24a5953e0f core: return FilteredSelector from attachments.ofNote 2023-11-27 10:01:20 +05:00
Abdullah Atta
ec1e868510 web: show notebook & tag shortcuts when assigning notes 2023-11-27 10:01:20 +05:00
Abdullah Atta
7450585737 web: implement virtualized search for all items 2023-11-27 10:01:20 +05:00
Abdullah Atta
9a09332182 web: use lazy loading to load attachments 2023-11-27 10:01:12 +05:00
Abdullah Atta
3992aa5dc0 web: load tag & color title lazily 2023-11-27 10:01:12 +05:00
Abdullah Atta
14f9d0ac7f web: show all nested notebooks as tree when moving notes 2023-11-27 10:01:12 +05:00
Abdullah Atta
f1a615515b web: add support for nested notebooks 2023-11-27 10:01:11 +05:00
Abdullah Atta
d4ff1dd46c web: use new virtualized grouping for reminders 2023-11-27 10:01:11 +05:00
Abdullah Atta
6276bd5db3 web: use new virtualized grouping for trash 2023-11-27 10:01:11 +05:00
Abdullah Atta
34736307c3 web: use new virtualized grouping for monographs 2023-11-27 10:01:11 +05:00
Abdullah Atta
60056f3fab core: add support for fetching trash as grouped 2023-11-27 10:01:11 +05:00
Abdullah Atta
f99a00789b web: install missing @hazae41/foras 2023-11-27 10:01:11 +05:00
Abdullah Atta
fcfda03fb2 core: fix attachment extraction for locked notes 2023-11-27 10:01:11 +05:00
Abdullah Atta
cd4c2934e0 global: update lockfiles 2023-11-27 10:01:10 +05:00
Abdullah Atta
396508d4b9 web: start initial migration to sqlite 2023-11-27 10:01:10 +05:00
Abdullah Atta
ffc03460b9 ui: add support for lazily loading menu items 2023-11-27 10:01:10 +05:00
Abdullah Atta
4315503893 common: remove total-notes utility 2023-11-27 10:01:10 +05:00
Abdullah Atta
1c9e42fbfc core: changes according to client needs 2023-11-27 10:01:10 +05:00
Abdullah Atta
f8f992d11e core: fix database migration 2023-11-27 10:01:10 +05:00
Abdullah Atta
8aa36f632a core: add support for incremental grouping 2023-11-27 10:01:10 +05:00
Abdullah Atta
ac89e3851a core: fix size of nanoid in benchmarks 2023-11-27 10:01:10 +05:00
Abdullah Atta
430ad22263 core: optimize bson objectid generation & add benchmarks 2023-11-27 10:01:09 +05:00
Abdullah Atta
c46ba25408 core: fix monographs 2023-11-27 10:01:09 +05:00
Abdullah Atta
ddcddbc8a8 core: fix sync tests 2023-11-27 10:01:09 +05:00
Abdullah Atta
24b1ada636 core: remove temporary sqlite db 2023-11-27 10:01:09 +05:00
Abdullah Atta
98a3b12bb8 core: migrate everything to sqlite 2023-11-27 10:01:09 +05:00
Abdullah Atta
26dca8b1d8 core: migrate all database writes to sqlite 2023-11-27 10:00:24 +05:00
Abdullah Atta
7dc9529981 global: update lockfiles 2023-11-27 10:00:05 +05:00
Abdullah Atta
32e36325b3 core: remove outbox 2023-11-27 10:00:05 +05:00
Abdullah Atta
7b554bf781 core: convert everything else to typescript 2023-11-27 10:00:05 +05:00
Abdullah Atta
4bcc08d046 core: remove unnecessary sync tests 2023-11-27 09:57:25 +05:00
Abdullah Atta
77bb9726ab core: migrate monographs to typescript 2023-11-27 09:57:25 +05:00
Abdullah Atta
94f3dc0c52 core: make migration tests less flaky 2023-11-27 09:57:25 +05:00
Abdullah Atta
be19ff1171 core: add tests for migrations 2023-11-27 09:57:25 +05:00
Abdullah Atta
6d52f9a6a3 core: migrate settings to its own collection 2023-11-27 09:57:25 +05:00
Abdullah Atta
b3c6bedc13 core: get rid of topics; move to sub notebooks 2023-11-27 09:57:25 +05:00
Abdullah Atta
94bc61f636 core: migrate attachments to use relations for linking to notes 2023-11-27 09:57:25 +05:00
Abdullah Atta
68406a2a3a core: minor updates and fixes 2023-11-27 09:57:25 +05:00
Abdullah Atta
8a0bf20e11 core: fix types & syncing issues 2023-11-27 09:57:25 +05:00
ammarahm-ed
1055b6f13a mobile: pass all tests 2023-11-27 09:55:56 +05:00
ammarahm-ed
4994ad77b4 mobile: fix filesystem not loaded 2023-11-27 09:55:56 +05:00
ammarahm-ed
e0712ce2f3 mobile: fix navigating to tags 2023-11-27 09:55:56 +05:00
ammarahm-ed
559801ba80 mobile: migrate to typescript 2023-11-27 09:55:56 +05:00
ammarahm-ed
b69dd3b217 common: fix ts errors 2023-11-27 09:50:01 +05:00
Abdullah Atta
8328575806 mobile: use new types from core 2023-11-27 09:50:01 +05:00
Abdullah Atta
30de9cabe0 web: use new types from core 2023-11-27 09:50:00 +05:00
Abdullah Atta
506cb2dd47 core: fix more typings 2023-11-27 09:49:31 +05:00
Abdullah Atta
2f5a3db1c1 core: fix typing errors 2023-11-27 09:49:11 +05:00
Abdullah Atta
06f4fee088 core: convert almost everything to typescript 2023-11-27 09:49:11 +05:00
606 changed files with 107220 additions and 80401 deletions

View File

@@ -78,7 +78,13 @@ module.exports = {
semi: ["error", "always"],
"@typescript-eslint/no-empty-function": "off",
"react/prop-types": "off",
"header/header": ["error", "block", LICENSE, 1]
"header/header": ["error", "block", LICENSE, 1],
"@typescript-eslint/no-empty-interface": [
"error",
{
allowSingleExtends: true
}
]
},
settings: {
react: {

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "@azure/core-asynciterator-polyfill";
import "react-native-gesture-handler";
import SettingsService from "./services/settings";
import {
THEME_COMPATIBILITY_VERSION,
@@ -24,7 +25,6 @@ import {
} from "@notesnook/theme";
import React, { useEffect } from "react";
import { View } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import AppLockedOverlay from "./components/app-lock-overlay";
@@ -38,14 +38,14 @@ import { useThemeStore } from "./stores/use-theme-store";
import { useUserStore } from "./stores/use-user-store";
import { themeTrpcClient } from "./screens/settings/theme-selector";
SettingsService.checkOrientation();
const App = () => {
const init = useAppEvents();
useEffect(() => {
let { appLockMode } = SettingsService.get();
const { appLockMode } = SettingsService.get();
if (appLockMode && appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
//@ts-ignore
globalThis["IS_MAIN_APP_RUNNING"] = true;
init();
setTimeout(async () => {
@@ -97,10 +97,10 @@ const App = () => {
let currTheme =
useThemeStore.getState().colorScheme === "dark"
? SettingsService.getProperty("darkTheme")
: SettingsService.getProperty("lightTheme");
: SettingsService.getProperty("lighTheme");
useThemeEngineStore.getState().setTheme(currTheme);
export const withTheme = (Element) => {
export const withTheme = (Element: () => JSX.Element) => {
return function AppWithThemeProvider() {
const [colorScheme, darkTheme, lightTheme] = useThemeStore((state) => [
state.colorScheme,

View File

@@ -16,15 +16,17 @@ 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 "./logger";
import { database } from "@notesnook/common";
import { logger as dbLogger } from "@notesnook/core/dist/logger";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
import filesystem from "../filesystem";
import "./logger";
import Storage from "./storage";
import { RNSqliteDriver } from "./sqlite.kysely";
database.host(
__DEV__
@@ -49,15 +51,30 @@ database.host(
}
);
database.setup(
Storage,
Platform.OS === "ios" ? EventSource : AndroidEventSource,
filesystem,
{
database.setup({
storage: Storage,
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
fs: filesystem,
compressor: {
compress: Gzip.deflate,
decompress: Gzip.inflate
},
batchSize: 500,
sqliteOptions: {
dialect: {
createDriver: () =>
new RNSqliteDriver({ async: true, dbName: "test.db" }),
createAdapter: () => new SqliteAdapter(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler()
}
// journalMode: "MEMORY",
// synchronous: "normal",
// pageSize: 8192,
// cacheSize: -16000,
// lockingMode: "exclusive"
}
);
});
export const db = database;
export const DatabaseLogger = dbLogger;

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ import * as ScopedStorage from "react-native-scoped-storage";
import { subscribe, zip } from "react-native-zip-archive";
import RNFetchBlob from "react-native-blob-util";
import { ShareComponent } from "../../components/sheets/export-notes/share";
import { ToastEvent, presentSheet } from "../../services/event-manager";
import { ToastManager, presentSheet } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import Storage from "../database/storage";
@@ -186,7 +186,7 @@ export default async function downloadAttachment(
) {
await createCacheDir();
let attachment = db.attachments.attachment(hash);
let attachment = await db.attachments.attachment(hash);
if (!attachment) {
console.log("attachment not found");
return;
@@ -208,10 +208,12 @@ export default async function downloadAttachment(
attachment.metadata.hash,
options.groupId
);
await db.fs.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
await db
.fs()
.downloadFile(
options.groupId || attachment.metadata.hash,
attachment.metadata.hash
);
if (
!(await RNFetchBlob.fs.exists(`${cacheDir}/${attachment.metadata.hash}`))
)
@@ -243,7 +245,7 @@ export default async function downloadAttachment(
);
if (!options.silent) {
ToastEvent.show({
ToastManager.show({
heading: "Download successful",
message: filename + " downloaded",
type: "success"

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import hosts from "@notesnook/core/dist/utils/constants";
import NetInfo from "@react-native-community/netinfo";
import RNFetchBlob from "react-native-blob-util";
import { ToastEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
import { db } from "../database";
import { cacheDir, fileCheck } from "./utils";
@@ -74,13 +74,13 @@ export async function downloadFile(filename, data, cancelToken) {
return status >= 200 && status < 300;
} catch (e) {
if (e.message !== "canceled") {
ToastEvent.show({
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
context: "global"
});
ToastEvent.show({
ToastManager.show({
heading: "Error downloading file",
message: e.message,
type: "error",
@@ -98,7 +98,7 @@ export async function downloadFile(filename, data, cancelToken) {
export async function getUploadedFileSize(hash) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
const token = await db.user.tokenManager.getAccessToken();
const token = await db.tokenManager.getAccessToken();
const attachmentInfo = await fetch(url, {
method: "HEAD",

View File

@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useCallback, useEffect, useRef } from "react";
import { Platform, View } from "react-native";
import { Platform, TextInput, View } from "react-native";
//@ts-ignore
import { enabled } from "react-native-privacy-snapshot";
import { db } from "../../common/database";
import { useAppState } from "../../hooks/use-app-state";
@@ -41,8 +42,8 @@ const AppLockedOverlay = () => {
const appLocked = useUserStore((state) => state.appLocked);
const lockApp = useUserStore((state) => state.lockApp);
const deviceMode = useSettingStore((state) => state.deviceMode);
const passwordInputRef = useRef();
const password = useRef();
const passwordInputRef = useRef<TextInput>(null);
const password = useRef<string>();
const appState = useAppState();
const biometricUnlockAwaitingUserInput = useRef(false);
@@ -55,14 +56,14 @@ const AppLockedOverlay = () => {
}
useSettingStore.getState().setRequestBiometrics(true);
let unlocked = await BiometricService.validateUser(
const unlocked = await BiometricService.validateUser(
"Unlock to access your notes",
""
);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = null;
password.current = undefined;
}
setTimeout(() => {
biometricUnlockAwaitingUserInput.current = false;
@@ -73,11 +74,11 @@ const AppLockedOverlay = () => {
const onSubmit = async () => {
if (!password.current) return;
try {
let unlocked = await db.user.verifyPassword(password.current);
const unlocked = await db.user.verifyPassword(password.current);
if (unlocked) {
lockApp(false);
enabled(false);
password.current = null;
password.current = undefined;
}
} catch (e) {
console.error(e);
@@ -170,7 +171,9 @@ const AppLockedOverlay = () => {
secureTextEntry
placeholder="Enter account password"
onChangeText={(v) => (password.current = v)}
onSubmit={onSubmit}
onSubmit={() => {
onSubmit();
}}
/>
</>
) : null}

View File

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

View File

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

View File

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

View File

@@ -17,16 +17,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useRef, useState } from "react";
import {
Attachment,
Note,
SortOptions,
VirtualizedGrouping
} from "@notesnook/core";
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useRef, useState } from "react";
import { ActivityIndicator, ScrollView, View } from "react-native";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import filesystem from "../../common/filesystem";
import { presentSheet } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import Input from "../ui/input";
import Seperator from "../ui/seperator";
@@ -34,79 +43,77 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AttachmentItem } from "./attachment-item";
import DownloadAttachments from "./download-attachments";
import { Button } from "../ui/button";
import {
isAudio,
isDocument,
isImage,
isVideo
} from "@notesnook/core/dist/utils/filename";
import { useSettingStore } from "../../stores/use-setting-store";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
export const AttachmentDialog = ({ note }) => {
const DEFAULT_SORTING: SortOptions = {
sortBy: "dateEdited",
sortDirection: "desc"
};
export const AttachmentDialog = ({ note }: { note?: Note }) => {
const { colors } = useThemeColors();
const { height } = useSettingStore((state) => state.dimensions);
const [attachments, setAttachments] = useState(
note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])]
);
const attachmentSearchValue = useRef();
const searchTimer = useRef();
const [attachments, setAttachments] =
useState<VirtualizedGrouping<Attachment>>();
const attachmentSearchValue = useRef<string>();
const searchTimer = useRef<NodeJS.Timeout>();
const [loading, setLoading] = useState(false);
const [currentFilter, setCurrentFilter] = useState("all");
const onChangeText = (text) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
const refresh = React.useCallback(() => {
if (note) {
db.attachments.ofNote(note.id, "all").sorted(DEFAULT_SORTING);
} else {
db.attachments.all
.sorted(DEFAULT_SORTING)
.then((attachments) => setAttachments(attachments));
}
}, [note]);
useEffect(() => {
refresh();
}, [note, refresh]);
const onChangeText = (text: string) => {
attachmentSearchValue.current = text;
if (
!attachmentSearchValue.current ||
attachmentSearchValue.current === ""
) {
setAttachments([...attachments]);
refresh();
}
clearTimeout(searchTimer.current);
searchTimer.current = setTimeout(() => {
let results = db.lookup.attachments(
attachments,
attachmentSearchValue.current
);
if (results.length === 0) return;
searchTimer.current = setTimeout(async () => {
const results = await db.lookup
.attachments(attachmentSearchValue.current as string)
.sorted();
setAttachments(results);
}, 300);
};
const renderItem = ({ item }) => (
const renderItem = ({ item }: { item: string | number }) => (
<AttachmentItem
setAttachments={setAttachments}
attachment={item}
attachments={attachments}
id={item}
context="attachments-list"
/>
);
const onCheck = async () => {
if (!attachments) return;
setLoading(true);
const checkedAttachments = [];
for (let attachment of attachments) {
let result = await filesystem.checkAttachment(attachment.metadata.hash);
for (const id of attachments.ids) {
const attachment = (await attachments.item(id))?.item;
if (!attachment) continue;
const result = await filesystem.checkAttachment(attachment.hash);
if (result.failed) {
await db.attachments.markAsFailed(
attachment.metadata.hash,
result.failed
);
await db.attachments.markAsFailed(attachment.hash, result.failed);
} else {
await db.attachments.markAsFailed(attachment.id, null);
await db.attachments.markAsFailed(attachment.id, undefined);
}
checkedAttachments.push(
db.attachments.attachment(attachment.metadata.hash)
);
setAttachments([...checkedAttachments]);
}
refresh();
setLoading(false);
};
@@ -133,31 +140,33 @@ export const AttachmentDialog = ({ note }) => {
}
];
const filterAttachments = (type) => {
const attachments = note
? db.attachments.ofNote(note.id, "all")
: [...(db.attachments.all || [])];
isDocument;
const filterAttachments = async (type: string) => {
let items: FilteredSelector<Attachment> = db.attachments.all;
switch (type) {
case "all":
return attachments;
items = db.attachments.all;
break;
case "images":
return attachments.filter((attachment) =>
isImage(attachment.metadata.type)
);
items = note
? db.attachments.ofNote(note.id, "images")
: db.attachments.images;
break;
case "video":
return attachments.filter((attachment) =>
isVideo(attachment.metadata.type)
);
items = items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.videos;
break;
case "audio":
return attachments.filter((attachment) =>
isAudio(attachment.metadata.type)
);
items = db.attachments.all;
break;
case "documents":
return attachments.filter((attachment) =>
isDocument(attachment.metadata.type)
);
items = note
? db.attachments.ofNote(note.id, "all")
: db.attachments.documents;
}
return await items.sorted(DEFAULT_SORTING);
};
return (
@@ -215,6 +224,7 @@ export const AttachmentDialog = ({ note }) => {
}}
color={colors.primary.paragraph}
onPress={() => {
if (!attachments) return;
DownloadAttachments.present(
"attachments-list",
attachments,
@@ -231,7 +241,7 @@ export const AttachmentDialog = ({ note }) => {
placeholder="Filter attachments by filename, type or hash"
onChangeText={onChangeText}
onSubmit={() => {
onChangeText(attachmentSearchValue.current);
onChangeText(attachmentSearchValue.current as string);
}}
/>
@@ -253,10 +263,7 @@ export const AttachmentDialog = ({ note }) => {
<Button
type={currentFilter === item.filterBy ? "grayAccent" : "gray"}
key={item.title}
title={
item.title +
` (${filterAttachments(item.filterBy)?.length || 0})`
}
title={item.title}
style={{
borderRadius: 0,
borderBottomWidth: 1,
@@ -266,9 +273,9 @@ export const AttachmentDialog = ({ note }) => {
? "transparent"
: colors.primary.accent
}}
onPress={() => {
onPress={async () => {
setCurrentFilter(item.filterBy);
setAttachments(filterAttachments(item.filterBy));
setAttachments(await filterAttachments(item.filterBy));
}}
/>
))}
@@ -299,7 +306,7 @@ export const AttachmentDialog = ({ note }) => {
/>
}
estimatedItemSize={50}
data={attachments}
data={attachments?.ids}
renderItem={renderItem}
/>
@@ -322,7 +329,7 @@ export const AttachmentDialog = ({ note }) => {
);
};
AttachmentDialog.present = (note) => {
AttachmentDialog.present = (note?: Note) => {
presentSheet({
component: () => <AttachmentDialog note={note} />
});

View File

@@ -23,7 +23,7 @@ import { db } from "../../common/database";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eCloseSheet } from "../../utils/events";
@@ -45,7 +45,7 @@ export const ChangePassword = () => {
const changePassword = async () => {
if (!user?.isEmailConfirmed) {
ToastEvent.show({
ToastManager.show({
heading: "Email not confirmed",
message: "Please confirm your email to change account password",
type: "error",
@@ -54,7 +54,7 @@ export const ChangePassword = () => {
return;
}
if (error || !oldPassword.current || !password.current) {
ToastEvent.show({
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again.",
type: "error",
@@ -66,7 +66,7 @@ export const ChangePassword = () => {
try {
await db.user.clearSessions();
await db.user.changePassword(oldPassword.current, password.current);
ToastEvent.show({
ToastManager.show({
heading: "Account password updated",
type: "success",
context: "global"
@@ -75,7 +75,7 @@ export const ChangePassword = () => {
eSendEvent(eCloseSheet);
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Failed to change password",
message: e.message,
type: "error",

View File

@@ -22,7 +22,7 @@ import { View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import SettingsService from "../../services/settings";
import { useThemeColors } from "@notesnook/theme";
import DialogHeader from "../dialog/dialog-header";
@@ -43,7 +43,7 @@ export const ForgotPassword = () => {
const sendRecoveryEmail = async () => {
if (!email.current || error) {
ToastEvent.show({
ToastManager.show({
heading: "Account email is required.",
type: "error",
context: "local"
@@ -63,7 +63,7 @@ export const ForgotPassword = () => {
SettingsService.set({
lastRecoveryEmailTime: Date.now()
});
ToastEvent.show({
ToastManager.show({
heading: "Check your email to reset password",
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
type: "success",
@@ -74,7 +74,7 @@ export const ForgotPassword = () => {
setSent(true);
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Recovery email not sent",
message: e.message,
type: "error",

View File

@@ -24,7 +24,7 @@ import { db } from "../../common/database";
import { MMKV } from "../../common/database/mmkv";
import BiometricService from "../../services/biometrics";
import {
ToastEvent,
ToastManager,
eSendEvent,
eSubscribeEvent
} from "../../services/event-manager";
@@ -85,7 +85,7 @@ export const SessionExpired = () => {
clearAllStores();
setVisible(false);
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: e.message,
type: "error",
context: "local"
@@ -102,9 +102,9 @@ export const SessionExpired = () => {
const open = React.useCallback(async () => {
try {
let res = await db.user.tokenManager.getToken();
let res = await db.tokenManager.getToken();
if (!res) throw new Error("no token found");
if (db.user.tokenManager._isTokenExpired(res))
if (db.tokenManager._isTokenExpired(res))
throw new Error("token expired");
Sync.run("global", false, true, async (complete) => {
if (!complete) {

View File

@@ -21,7 +21,7 @@ import React, { useRef, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastEvent } from "../../services/event-manager";
import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -52,7 +52,7 @@ export const Signup = ({ changeMode, trial }) => {
const { width, height } = useWindowDimensions();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastEvent.show({
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -85,7 +85,7 @@ export const Signup = ({ changeMode, trial }) => {
}
} catch (e) {
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Signup failed",
message: e.message,
type: "error",

View File

@@ -24,7 +24,7 @@ import useTimer from "../../hooks/use-timer";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
@@ -138,7 +138,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
setSending(false);
} catch (e) {
setSending(false);
ToastEvent.error(e, "Error sending 2FA Code", "local");
ToastManager.error(e, "Error sending 2FA Code", "local");
}
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useRef, useState } from "react";
import { db } from "../../common/database";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { clearMessage } from "../../services/message";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
@@ -48,7 +48,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
(!password.current && step === LoginSteps.passwordAuth) ||
(!email.current && step === LoginSteps.emailAuth)
) {
ToastEvent.show({
ToastManager.show({
heading: "All fields required",
message: "Fill all the fields and try again",
type: "error",
@@ -121,7 +121,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
const finishWithError = async (e) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastEvent.show({
ToastManager.show({
heading: "Login failed",
message: e.message,
type: "error",
@@ -135,7 +135,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
PremiumService.setPremiumStatus();
setUser(user);
clearMessage();
ToastEvent.show({
ToastManager.show({
heading: "Login successful",
message: `Logged in as ${user.email}`,
type: "success",

View File

@@ -1,40 +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, { PropsWithChildren } from "react";
import { View } from "react-native";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
export const ContainerHeader = (props: PropsWithChildren) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
return !selectionMode ? (
<View
style={{
backgroundColor: colors.primary.background,
width: "100%",
overflow: "hidden"
}}
>
{props.children}
</View>
) : null;
};

View File

@@ -36,10 +36,11 @@ import { SIZE, normalize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import { PressableButton } from "../ui/pressable";
interface FloatingButton {
title?: string;
interface FloatingButtonProps {
title: string;
onPress: () => void;
color?: string;
shouldShow?: boolean;
alwaysVisible?: boolean;
}
@@ -48,7 +49,7 @@ const FloatingButton = ({
onPress,
color,
alwaysVisible = false
}: FloatingButton) => {
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -118,7 +119,7 @@ const FloatingButton = ({
<PressableButton
testID={notesnook.buttons.add}
type="accent"
accentColor={colors.static[color as keyof typeof colors.static]}
accentColor={color}
customStyle={{
...getElevationStyle(5),
borderRadius: 100

View File

@@ -21,15 +21,9 @@ import React, { PropsWithChildren } from "react";
import { KeyboardAvoidingView, Platform } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import { useSettingStore } from "../../stores/use-setting-store";
import { Header } from "../header";
import SelectionHeader from "../selection-header";
export const Container = ({ children }: PropsWithChildren) => {
const floating = useIsFloatingKeyboard();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const insets = useGlobalSafeAreaInsets();
return (
@@ -43,13 +37,6 @@ export const Container = ({ children }: PropsWithChildren) => {
paddingBottom: Platform.OS === "android" ? 0 : insets.bottom
}}
>
{!introCompleted ? null : (
<>
<SelectionHeader />
<Header />
</>
)}
{children}
</KeyboardAvoidingView>
);

View File

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

View File

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

View File

@@ -17,8 +17,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
import {
ColorValue,
KeyboardAvoidingView,
Modal,
Platform,
@@ -35,6 +36,25 @@ import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { useAppState } from "../../hooks/use-app-state";
export interface BaseDialogProps extends PropsWithChildren {
animation?: "fade" | "none" | "slide" | undefined;
visible: boolean;
onRequestClose?: () => void;
onShow?: () => void;
premium?: boolean;
statusBarTranslucent?: boolean;
transparent?: boolean;
centered?: boolean;
bottom?: boolean;
background?: string | ColorValue;
animated?: boolean;
bounce?: boolean;
closeOnTouch?: boolean;
useSafeArea?: boolean;
avoidKeyboardResize?: boolean;
enableSheetKeyboardHandler?: boolean;
}
const BaseDialog = ({
visible,
onRequestClose,
@@ -46,14 +66,14 @@ const BaseDialog = ({
transparent,
centered = true,
bottom = false,
background = null,
animated = true,
bounce = true,
closeOnTouch = true,
useSafeArea = true,
avoidKeyboardResize = false,
enableSheetKeyboardHandler = false
}) => {
enableSheetKeyboardHandler,
background
}: BaseDialogProps) => {
const floating = useIsFloatingKeyboard();
const appState = useAppState();
const lockEvents = useRef(false);
@@ -147,7 +167,7 @@ const BaseDialog = ({
]}
>
<TouchableOpacity
onPress={closeOnTouch ? onRequestClose : null}
onPress={closeOnTouch ? onRequestClose : undefined}
style={styles.overlayButton}
/>
{premium}

View File

@@ -25,7 +25,7 @@ type DialogInfo = {
paragraph?: string;
positiveText?: string;
negativeText?: string;
positivePress?: (value: unknown) => void;
positivePress?: (value: any) => void;
onClose?: () => void;
positiveType?:
| "transparent"

View File

@@ -1,190 +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,
ToastEvent
} 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() === "") {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return;
}
if (!this.toEdit) {
await db.notebooks.notebook(this.notebook.id).topics.add(this.title);
} else {
let topic = this.toEdit;
topic.title = this.title;
await db.notebooks.notebook(topic.notebookId).topics.add(topic);
}
this.close();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
useMenuStore.getState().setMenuPins();
});
eSendEvent(eOnTopicSheetUpdate);
useRelationStore.getState().update();
} catch (e) {
console.error(e);
}
};
componentDidMount() {
eSubscribeEvent(eOpenAddTopicDialog, this.open);
eSubscribeEvent(eCloseAddTopicDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenAddTopicDialog, this.open);
eUnSubscribeEvent(eCloseAddTopicDialog, this.close);
}
open = async ({ notebookId, toEdit }) => {
let id = notebookId;
if (id) {
this.notebook = await db.notebooks.notebook(id).data;
}
this.toEdit = toEdit;
if (this.toEdit) {
this.title = this.toEdit.title;
}
this.setState({
visible: true
});
};
close = () => {
this.title = null;
this.notebook = null;
this.toEdit = null;
this.setState({
visible: false
});
};
render() {
const { visible } = this.state;
if (!visible) return null;
return (
<BaseDialog
onShow={async () => {
if (this.toEdit) {
this.titleRef.current?.setNativeProps({
text: this.toEdit.title
});
}
this.ref.current?.animateNextTransition();
await sleep(300);
this.titleRef.current?.focus();
}}
bounce={false}
statusBarTranslucent={false}
visible={true}
onRequestClose={this.close}
>
<DialogContainer>
<DialogHeader
icon="book-outline"
title={this.toEdit ? "Edit topic" : "New topic"}
paragraph={
this.toEdit
? "Edit title of the topic"
: "Add a new topic in " + this.notebook.title
}
padding={12}
/>
<Seperator half />
<View
style={{
paddingHorizontal: 12,
zIndex: 10
}}
>
<Input
fwdRef={this.titleRef}
testID="input-title"
onChangeText={(value) => {
this.title = value;
}}
blurOnSubmit={false}
placeholder="Enter title"
onSubmit={() => this.addNewTopic()}
returnKeyLabel="Done"
returnKeyType="done"
/>
</View>
<DialogButtons
positiveTitle={this.toEdit ? "Save" : "Add"}
onPressNegative={() => this.close()}
onPressPositive={() => this.addNewTopic()}
/>
</DialogContainer>
<Toast context="local" />
</BaseDialog>
);
}
}

View File

@@ -17,15 +17,22 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import { GroupHeader, Item, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import { FlatList, ScrollView, View } from "react-native";
import { DDS } from "../../../services/device-detection";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useMessageStore } from "../../../stores/use-message-store";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../../utils/elevation";
import {
eCloseJumpToDialog,
@@ -36,27 +43,46 @@ import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { useCallback } from "react";
const offsets = [];
let timeout = null;
const JumpToSectionDialog = ({ scrollRef, data, type }) => {
const JumpToSectionDialog = () => {
const scrollRef = useRef<RefObject<FlatList>>();
const [data, setData] = useState<VirtualizedGrouping<Item>>();
const { colors } = useThemeColors();
const notes = data;
const [visible, setVisible] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const currentScrollPosition = useRef(0);
const [groups, setGroups] = useState<
{
index: number;
group: GroupHeader;
}[]
>();
const offsets = useRef<number[]>([]);
const onPress = (item) => {
let ind = notes.findIndex(
(i) => i.title === item.title && i.type === "header"
);
scrollRef.current?.scrollToIndex({
index: ind,
const onPress = (item: { index: number; group: GroupHeader }) => {
scrollRef.current?.current?.scrollToIndex({
index: item.index,
animated: true
});
close();
};
const open = useCallback(
({
data,
ref
}: {
data: VirtualizedGrouping<Item>;
ref: RefObject<FlatList>;
}) => {
setData(data);
scrollRef.current = ref;
setVisible(true);
},
[]
);
useEffect(() => {
eSubscribeEvent(eOpenJumpToDialog, open);
eSubscribeEvent(eCloseJumpToDialog, close);
@@ -69,54 +95,45 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
};
}, [open]);
const onScroll = (data) => {
let y = data.y;
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
timeout = setTimeout(() => {
let index = offsets.findIndex((o, i) => o <= y && offsets[i + 1] > y);
setCurrentIndex(index || 0);
}, 200);
const onScroll = (data: { x: number; y: number }) => {
currentScrollPosition.current = data.y;
};
const open = useCallback(
(_type) => {
if (_type !== type) return;
setVisible(true);
},
[type]
);
const close = () => {
setVisible(false);
};
useEffect(() => {
loadOffsets();
}, [loadOffsets, notes]);
const loadOffsets = useCallback(() => {
notes
.filter((i) => i.type === "header")
.map((item, index) => {
const loadGroupsAndOffsets = useCallback(() => {
notes?.groups?.().then((groups) => {
setGroups(groups);
offsets.current = [];
groups.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);
let groupIndex = item.index;
const messageState = useMessageStore.getState().message;
const msgOffset = messageState?.visible ? 60 : 10;
groupIndex = groupIndex + 1;
groupIndex = groupIndex - (index + 1);
offset = offset + groupIndex * 100 + msgOffset;
offsets.current.push(offset);
});
const index = offsets.current?.findIndex((o, i) => {
return (
o <= currentScrollPosition.current + 100 &&
offsets.current[i + 1] - 100 > currentScrollPosition.current
);
});
setCurrentIndex(index < 0 ? 0 : index);
});
}, [notes]);
return !visible ? null : (
<BaseDialog
onShow={() => {
loadOffsets();
loadGroupsAndOffsets();
}}
onRequestClose={close}
visible={true}
@@ -149,40 +166,38 @@ const JumpToSectionDialog = ({ scrollRef, data, type }) => {
paddingBottom: 20
}}
>
{notes
.filter((i) => i.type === "header")
.map((item, index) => {
return item.title ? (
<PressableButton
key={item.title}
onPress={() => onPress(item, index)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 25,
marginVertical: 10
{groups?.map((item, index) => {
return (
<PressableButton
key={item.group.id}
onPress={() => onPress(item)}
type={currentIndex === index ? "selected" : "transparent"}
customStyle={{
minWidth: "20%",
width: null,
paddingHorizontal: 12,
margin: 5,
borderRadius: 100,
height: 25,
marginVertical: 10
}}
>
<Paragraph
size={SIZE.sm}
color={
currentIndex === index
? colors.selected.accent
: colors.primary.accent
}
style={{
textAlign: "center"
}}
>
<Paragraph
size={SIZE.sm}
color={
currentIndex === index
? colors.selected.accent
: colors.primary.accent
}
style={{
textAlign: "center"
}}
>
{item.title}
</Paragraph>
</PressableButton>
) : null;
})}
{item.group.title}
</Paragraph>
</PressableButton>
);
})}
</View>
</ScrollView>
</View>

View File

@@ -27,14 +27,12 @@ import { editorController } from "../../../screens/editor/tiptap/utils";
import BiometricService from "../../../services/biometrics";
import { DDS } from "../../../services/device-detection";
import {
ToastManager,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
eUnSubscribeEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { convertNoteToText } from "../../../utils/note-to-text";
import { getElevationStyle } from "../../../utils/elevation";
import {
eClearEditor,
@@ -45,6 +43,7 @@ import {
} from "../../../utils/events";
import { deleteItems } from "../../../utils/functions";
import { tabBarRef } from "../../../utils/global-refs";
import { convertNoteToText } from "../../../utils/note-to-text";
import { sleep } from "../../../utils/time";
import BaseDialog from "../../dialog/base-dialog";
import DialogButtons from "../../dialog/dialog-buttons";
@@ -55,10 +54,6 @@ import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
let Keychain;
const passInputRef = createRef();
const confirmPassRef = createRef();
const changePassInputRef = createRef();
export class VaultDialog extends Component {
constructor(props) {
super(props);
@@ -88,6 +83,11 @@ export class VaultDialog extends Component {
deleteVault: false,
deleteAll: false
};
this.passInputRef = createRef();
this.confirmPassRef = createRef();
this.changePassInputRef = createRef();
this.password = null;
this.confirmPassword = null;
this.newPassword = null;
@@ -148,9 +148,6 @@ export class VaultDialog extends Component {
* @param {import('../../../services/event-manager').vaultType} data
*/
open = async (data) => {
if (!Keychain) {
Keychain = require("react-native-keychain");
}
let biometry = await BiometricService.isBiometryAvailable();
let available = false;
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
@@ -199,7 +196,7 @@ export class VaultDialog extends Component {
close = () => {
if (this.state.loading) {
ToastEvent.show({
ToastManager.show({
heading: this.state.title,
message: "Please wait and do not close the app.",
type: "success",
@@ -212,7 +209,6 @@ export class VaultDialog extends Component {
this.password = null;
this.confirmPassword = null;
SearchService.updateAndSearch();
this.setState({
visible: false,
note: {},
@@ -236,7 +232,7 @@ export class VaultDialog extends Component {
if (this.state.loading) return;
if (!this.password) {
ToastEvent.show({
ToastManager.show({
heading: "Password not entered",
message: "Enter a password for the vault and try again.",
type: "error",
@@ -245,7 +241,7 @@ export class VaultDialog extends Component {
return;
}
if (this.password && this.password.length < 3) {
ToastEvent.show({
ToastManager.show({
heading: "Password too short",
message: "Password must be longer than 3 characters.",
type: "error",
@@ -257,7 +253,7 @@ export class VaultDialog extends Component {
if (!this.state.novault) {
if (this.password !== this.confirmPassword) {
ToastEvent.show({
ToastManager.show({
heading: "Passwords do not match",
type: "error",
context: "local"
@@ -283,7 +279,7 @@ export class VaultDialog extends Component {
if (this.state.biometricUnlock) {
this._enrollFingerprint(this.newPassword);
}
ToastEvent.show({
ToastManager.show({
heading: "Vault password updated successfully",
type: "success",
context: "global"
@@ -295,17 +291,19 @@ export class VaultDialog extends Component {
loading: false
});
if (e.message === db.vault.ERRORS.wrongPassword) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
context: "local"
});
} else {
ToastManager.error(e);
}
});
} else if (this.state.locked) {
if (!this.password || this.password.trim() === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -355,7 +353,7 @@ export class VaultDialog extends Component {
});
this.close();
} else {
ToastEvent.show({
ToastManager.show({
heading: "Account password incorrect",
message: "Please enter correct password for your account.",
type: "error",
@@ -382,7 +380,7 @@ export class VaultDialog extends Component {
this.close();
eSendEvent("vaultUpdated");
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: "Vault password incorrect",
message: "Please enter correct password to clear vault.",
type: "error",
@@ -396,7 +394,7 @@ export class VaultDialog extends Component {
async _lockNote() {
if (!this.password || this.password.trim() === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -408,7 +406,7 @@ export class VaultDialog extends Component {
eSendEvent(eClearEditor);
}
this.close();
ToastEvent.show({
ToastManager.show({
message: "Note locked successfully",
type: "error",
context: "local"
@@ -421,7 +419,7 @@ export class VaultDialog extends Component {
async _unlockNote() {
if (!this.password || this.password.trim() === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message: "Please enter the correct password and try again",
type: "error",
@@ -459,7 +457,7 @@ export class VaultDialog extends Component {
async _deleteNote() {
try {
await db.vault.remove(this.state.note.id, this.password);
await deleteItems(this.state.note);
await deleteItems([this.state.note.id], "note");
this.close();
} catch (e) {
this._takeErrorAction(e);
@@ -479,7 +477,7 @@ export class VaultDialog extends Component {
loading: false
});
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Biometric unlocking enabled!",
message: "Now you can unlock notes in vault with biometrics.",
type: "success",
@@ -488,7 +486,7 @@ export class VaultDialog extends Component {
this.close();
} catch (e) {
this.close();
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
message:
"Please enter the correct vault password to enable biometrics.",
@@ -517,7 +515,7 @@ export class VaultDialog extends Component {
this.setState({
loading: false
});
ToastEvent.show({
ToastManager.show({
heading: "Note added to vault",
type: "success",
context: "global"
@@ -525,7 +523,7 @@ export class VaultDialog extends Component {
this.close();
} else {
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Vault created successfully",
type: "success",
context: "global"
@@ -538,7 +536,7 @@ export class VaultDialog extends Component {
db.vault
.remove(this.state.note.id, this.password)
.then(() => {
ToastEvent.show({
ToastManager.show({
heading: "Note permanently unlocked.",
type: "success",
context: "global"
@@ -553,7 +551,9 @@ export class VaultDialog extends Component {
_openInEditor(note) {
this.close();
InteractionManager.runAfterInteractions(async () => {
eSendEvent(eOnLoadNote, note);
eSendEvent(eOnLoadNote, {
item: note
});
if (!DDS.isTab) {
tabBarRef.current?.goToPage(1);
}
@@ -562,7 +562,7 @@ export class VaultDialog extends Component {
async _copyNote(note) {
Clipboard.setString(await convertNoteToText(note));
ToastEvent.show({
ToastManager.show({
heading: "Note copied",
type: "success",
message: "Note has been copied to clipboard!",
@@ -594,7 +594,7 @@ export class VaultDialog extends Component {
visible: true
});
setTimeout(() => {
ToastEvent.show({
ToastManager.show({
heading: "Incorrect password",
type: "error",
context: "local"
@@ -609,13 +609,13 @@ export class VaultDialog extends Component {
try {
await BiometricService.resetCredentials();
eSendEvent("vaultUpdated");
ToastEvent.show({
ToastManager.show({
heading: "Biometric unlocking disabled!",
type: "success",
context: "global"
});
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: "Failed to disable Biometric unlocking.",
message: e.message,
type: "success",
@@ -667,7 +667,8 @@ export class VaultDialog extends Component {
<BaseDialog
onShow={async () => {
await sleep(100);
passInputRef.current?.focus();
this.passInputRef.current?.focus();
}}
statusBarTranslucent={false}
onRequestClose={this.close}
@@ -702,7 +703,7 @@ export class VaultDialog extends Component {
!this.state.revokeFingerprintAccess ? (
<>
<Input
fwdRef={passInputRef}
fwdRef={this.passInputRef}
editable={!loading}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
@@ -719,7 +720,7 @@ export class VaultDialog extends Component {
}
onSubmit={() => {
changePassword
? changePassInputRef.current?.focus()
? this.confirmPassRef.current?.focus()
: this.onPress;
}}
autoComplete="password"
@@ -771,7 +772,7 @@ export class VaultDialog extends Component {
<>
<Seperator half />
<Input
ref={changePassInputRef}
ref={this.confirmPassRef}
editable={!loading}
testID={notesnook.ids.dialogs.vault.changePwd}
autoCapitalize="none"
@@ -791,7 +792,7 @@ export class VaultDialog extends Component {
{!novault ? (
<View>
<Input
fwdRef={passInputRef}
fwdRef={this.passInputRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwd}
onChangeText={(value) => {
@@ -802,13 +803,13 @@ export class VaultDialog extends Component {
returnKeyType="next"
secureTextEntry
onSubmit={() => {
confirmPassRef.current?.focus();
this.confirmPassRef.current?.focus();
}}
placeholder="Password"
/>
<Input
fwdRef={confirmPassRef}
fwdRef={this.confirmPassRef}
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry

View File

@@ -17,42 +17,73 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform, StyleSheet, View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { SearchBar } from "../../screens/search/search-bar";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import useNavigationStore, {
RouteName
} from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { eScrollEvent } from "../../utils/events";
import { LeftMenus } from "./left-menus";
import { RightMenus } from "./right-menus";
import { Title } from "./title";
const _Header = () => {
type HeaderRightButton = {
title: string;
onPress: () => void;
};
export const Header = ({
renderedInRoute,
onLeftMenuButtonPress,
title,
titleHiddenOnRender,
headerRightButtons,
id,
accentColor,
isBeta,
canGoBack,
onPressDefaultRightButton,
hasSearch,
onSearch
}: {
onLeftMenuButtonPress?: () => void;
renderedInRoute: RouteName;
id?: string;
title: string;
headerRightButtons?: HeaderRightButton[];
titleHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
canGoBack?: boolean;
onPressDefaultRightButton?: () => void;
hasSearch?: boolean;
onSearch?: () => void;
}) => {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const [hide, setHide] = useState(true);
const [borderHidden, setBorderHidden] = useState(true);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const currentScreen = useNavigationStore(
(state) => state.currentScreen?.name
);
const isFocused = useNavigationStore((state) => state.focusedRouteId === id);
const onScroll = useCallback(
(data: { x: number; y: number }) => {
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!hide) return;
setHide(false);
if (!borderHidden) return;
setBorderHidden(false);
} else {
if (hide) return;
setHide(true);
if (borderHidden) return;
setBorderHidden(true);
}
},
[hide]
[borderHidden, id, renderedInRoute]
);
useEffect(() => {
@@ -60,9 +91,9 @@ const _Header = () => {
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [hide, onScroll]);
}, [borderHidden, onScroll]);
return selectionMode ? null : (
return selectionMode && isFocused ? null : (
<>
<View
style={[
@@ -72,29 +103,42 @@ const _Header = () => {
backgroundColor: colors.primary.background,
overflow: "hidden",
borderBottomWidth: 1,
borderBottomColor: hide
borderBottomColor: borderHidden
? "transparent"
: colors.secondary.background,
justifyContent: "space-between"
}
]}
>
{currentScreen === "Search" ? (
<SearchBar />
) : (
<>
<View style={styles.leftBtnContainer}>
<LeftMenus />
<Title />
</View>
<RightMenus />
</>
)}
<>
<View style={styles.leftBtnContainer}>
<LeftMenus
canGoBack={canGoBack}
onLeftButtonPress={onLeftMenuButtonPress}
/>
<Title
isHiddenOnRender={titleHiddenOnRender}
renderedInRoute={renderedInRoute}
id={id}
accentColor={accentColor}
title={title}
isBeta={isBeta}
/>
</View>
<RightMenus
renderedInRoute={renderedInRoute}
id={id}
headerRightButtons={headerRightButtons}
onPressDefaultRightButton={onPressDefaultRightButton}
search={hasSearch}
onSearch={onSearch}
/>
</>
</View>
</>
);
};
export const Header = React.memo(_Header, () => true);
const styles = StyleSheet.create({
container: {

View File

@@ -17,23 +17,29 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { notesnook } from "../../../e2e/test.ids";
import { DDS } from "../../services/device-detection";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { tabBarRef } from "../../utils/global-refs";
import { IconButton } from "../ui/icon-button";
export const LeftMenus = () => {
export const LeftMenus = ({
canGoBack,
onLeftButtonPress
}: {
canGoBack?: boolean;
onLeftButtonPress?: () => void;
}) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const canGoBack = useNavigationStore((state) => state.canGoBack);
const isTablet = deviceMode === "tablet";
const onLeftButtonPress = () => {
const _onLeftButtonPress = () => {
if (onLeftButtonPress) return onLeftButtonPress();
if (!canGoBack) {
if (tabBarRef.current?.isDrawerOpen()) {
Navigation.closeDrawer();
@@ -54,21 +60,17 @@ export const LeftMenus = () => {
height: 40,
width: 40,
borderRadius: 100,
marginLeft: -5,
marginRight: DDS.isLargeTablet() ? 10 : 7
}}
left={40}
top={40}
right={DDS.isLargeTablet() ? 10 : 10}
onPress={onLeftButtonPress}
onPress={_onLeftButtonPress}
onLongPress={() => {
Navigation.popToTop();
}}
name={canGoBack ? "arrow-left" : "menu"}
color={colors.primary.paragraph}
iconStyle={{
marginLeft: canGoBack ? -5 : 0
}}
/>
);
};

View File

@@ -20,43 +20,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useRef } from "react";
import { Platform, StyleSheet, View } from "react-native";
//@ts-ignore
import { useThemeColors } from "@notesnook/theme";
import Menu from "react-native-reanimated-material-menu";
import { notesnook } from "../../../e2e/test.ids";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import {
HeaderRightButton,
RouteName
} from "../../stores/use-navigation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
export const RightMenus = () => {
export const RightMenus = ({
headerRightButtons,
renderedInRoute,
id,
onPressDefaultRightButton,
search,
onSearch
}: {
headerRightButtons?: HeaderRightButton[];
renderedInRoute: RouteName;
id?: string;
onPressDefaultRightButton?: () => void;
search?: boolean;
onSearch?: () => void;
}) => {
const { colors } = useThemeColors();
const { colors: contextMenuColors } = useThemeColors("contextMenu");
const deviceMode = useSettingStore((state) => state.deviceMode);
const buttons = useNavigationStore((state) => state.headerRightButtons);
const currentScreen = useNavigationStore((state) => state.currentScreen.name);
const buttonAction = useNavigationStore((state) => state.buttonAction);
const menuRef = useRef<{
show: () => void;
hide: () => void;
}>();
const menuRef = useRef<Menu>(null);
return (
<View style={styles.rightBtnContainer}>
{!currentScreen.startsWith("Settings") ? (
{search ? (
<IconButton
onPress={async () => {
SearchService.prepareSearch();
Navigation.navigate(
{
name: "Search"
},
{}
);
}}
onPress={onSearch}
testID="icon-search"
name="magnify"
color={colors.primary.paragraph}
@@ -66,9 +69,9 @@ export const RightMenus = () => {
{deviceMode !== "mobile" ? (
<Button
onPress={buttonAction}
onPress={onPressDefaultRightButton}
testID={notesnook.ids.default.addBtn}
icon={currentScreen === "Trash" ? "delete" : "plus"}
icon={renderedInRoute === "Trash" ? "delete" : "plus"}
iconSize={SIZE.xl}
type="shade"
hitSlop={{
@@ -89,7 +92,7 @@ export const RightMenus = () => {
/>
) : null}
{buttons && buttons.length > 0 ? (
{headerRightButtons && headerRightButtons.length > 0 ? (
<Menu
ref={menuRef}
animationDuration={200}
@@ -111,7 +114,7 @@ export const RightMenus = () => {
/>
}
>
{buttons.map((item) => (
{headerRightButtons.map((item) => (
<Button
style={{
width: 150,

View File

@@ -20,103 +20,74 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import useNavigationStore from "../../stores/use-navigation-store";
import { eScrollEvent } from "../../utils/events";
import { SIZE } from "../../utils/size";
import Tag from "../ui/tag";
import Heading from "../ui/typography/heading";
const titleState: { [name: string]: boolean } = {};
export const Title = () => {
export const Title = ({
title,
isHiddenOnRender,
accentColor,
isBeta,
renderedInRoute,
id
}: {
title: string;
isHiddenOnRender?: boolean;
accentColor?: string;
isBeta?: boolean;
renderedInRoute: string;
id?: string;
}) => {
const { colors } = useThemeColors();
const currentScreen = useNavigationStore((state) => state.currentScreen);
const isNotebook = currentScreen.name === "Notebook";
const isTopic = currentScreen?.name === "TopicNotes";
const [hide, setHide] = useState(
isNotebook
? typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true
: false
);
const isHidden = titleState[currentScreen.id as string];
const notebook =
isTopic && currentScreen.notebookId
? db.notebooks?.notebook(currentScreen.notebookId)?.data
: null;
const title = currentScreen.title;
const isTag = currentScreen?.name === "TaggedNotes";
const [visible, setVisible] = useState(isHiddenOnRender);
const isTag = title.startsWith("#");
const onScroll = useCallback(
(data: { x: number; y: number }) => {
if (currentScreen.name !== "Notebook") {
setHide(false);
return;
}
(data: { x: number; y: number; id?: string; route: string }) => {
if (data.route !== renderedInRoute || data.id !== id) return;
if (data.y > 150) {
if (!hide) return;
titleState[currentScreen.id as string] = false;
setHide(false);
if (!visible) return;
setVisible(false);
} else {
if (hide) return;
titleState[currentScreen.id as string] = true;
setHide(true);
if (visible) return;
setVisible(true);
}
},
[currentScreen.id, currentScreen.name, hide]
[id, renderedInRoute, visible]
);
useEffect(() => {
if (currentScreen.name === "Notebook") {
const value =
typeof titleState[currentScreen.id as string] === "boolean"
? titleState[currentScreen.id as string]
: true;
setHide(value);
} else {
setHide(titleState[currentScreen.id as string]);
}
}, [currentScreen.id, currentScreen.name]);
useEffect(() => {
eSubscribeEvent(eScrollEvent, onScroll);
return () => {
eUnSubscribeEvent(eScrollEvent, onScroll);
};
}, [hide, onScroll]);
}, [visible, onScroll]);
function navigateToNotebook() {
if (!isTopic) return;
Notebook.navigate(notebook, true);
}
return (
<>
{!hide && !isHidden ? (
{!visible ? (
<Heading
onPress={navigateToNotebook}
numberOfLines={1}
size={SIZE.lg}
style={{
flexWrap: "wrap",
marginTop: Platform.OS === "ios" ? -1 : 0
}}
color={currentScreen.color || colors.primary.heading}
color={accentColor || colors.primary.heading}
>
{isTag ? (
<Heading size={SIZE.xl} color={colors.primary.accent}>
#
</Heading>
) : null}
{title}{" "}
{isTag ? title.slice(1) : title}{" "}
<Tag
visible={currentScreen.beta}
visible={isBeta}
text="BETA"
style={{
backgroundColor: "transparent"

View File

@@ -32,23 +32,22 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { IconButton } from "../ui/icon-button";
import { ProgressBarComponent } from "../ui/svg/lazy";
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
const ImagePreview = () => {
const { colors } = useThemeColors("dialog");
const [visible, setVisible] = useState(false);
const [image, setImage] = useState("");
const [image, setImage] = useState<string>();
const [loading, setLoading] = useState(false);
useEffect(() => {
eSubscribeEvent("ImagePreview", open);
return () => {
eUnSubscribeEvent("ImagePreview", open);
};
}, []);
const open = async (image) => {
const open = async (image: ImageAttributes) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
@@ -63,6 +62,7 @@ const ImagePreview = () => {
});
}
if (!hash) return;
//@ts-ignore // FIX ME
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
@@ -74,7 +74,7 @@ const ImagePreview = () => {
};
const close = () => {
setImage(null);
setImage(undefined);
setVisible(false);
};
@@ -88,7 +88,7 @@ const ImagePreview = () => {
backgroundColor: "black"
}}
>
{loading ? (
{loading || !image ? (
<View
style={{
flex: 1,

View File

@@ -77,14 +77,9 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
Navigation.replace("Notes", {
canGoBack: false
});
}, 1000);
}}
style={{
@@ -106,14 +101,9 @@ export const WelcomeNotice = () => {
SettingsService.set({
introCompleted: true
});
Navigation.replace(
{
name: "Notes"
},
{
canGoBack: false
}
);
Navigation.replace("Notes", {
canGoBack: false
});
await sleep(1000);
eSendEvent(eCloseLoading);
}}

View File

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

View File

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

View File

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

View File

@@ -17,21 +17,30 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getUpcomingReminder } from "@notesnook/core/dist/collections/reminders";
import { decode, EntityLevel } from "entities";
import {
BaseTrashItem,
Color,
Note,
Reminder,
TrashItem
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { EntityLevel, decode } from "entities";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import Notebook from "../../../screens/notebook";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import NotebookScreen from "../../../screens/notebook";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { useEditorStore } from "../../../stores/use-editor-store";
import useNavigationStore from "../../../stores/use-navigation-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../../utils/colors";
import { SIZE } from "../../../utils/size";
import {
NotebooksWithDateEdited,
TagsWithDateEdited
} from "../../list/list-item.wrapper";
import { Properties } from "../../properties";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
@@ -39,79 +48,39 @@ import { ReminderTime } from "../../ui/reminder-time";
import { TimeSince } from "../../ui/time-since";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useIsCompactModeEnabled } from "../../../hooks/use-is-compact-mode-enabled";
import { useEditorStore } from "../../../stores/use-editor-store";
function navigateToTag(item) {
const tag = db.tags.tag(item.id);
if (!tag) return;
TaggedNotes.navigate(tag, true);
}
const showActionSheet = (item) => {
Properties.present(item);
type NoteItemProps = {
item: Note | BaseTrashItem<Note>;
index: number;
tags?: TagsWithDateEdited;
notebooks?: NotebooksWithDateEdited;
color?: Color;
reminder?: Reminder;
attachmentsCount: number;
date: number;
isTrash?: boolean;
noOpen?: boolean;
};
function getNotebook(item) {
const isTrash = item.type === "trash";
const currentId = useNavigationStore.getState().currentScreen.id;
if (isTrash) return [];
const items = [];
const notebooks = db.relations.to(item, "notebook") || [];
for (let notebook of notebooks) {
if (items.length > 1) break;
if (notebook.id === currentId) continue;
items.push(notebook);
}
if (item.notebooks) {
for (let nb of item.notebooks) {
if (items.length > 1) break;
const notebook = db.notebooks?.notebook(nb.id)?.data;
if (!notebook) continue;
for (let topicId of nb.topics) {
if (items.length > 1) break;
if (topicId === currentId) continue;
const topic = notebook.topics.find((t) => t.id === topicId);
if (!topic) continue;
items.push(topic);
}
}
}
return items;
}
function getTags(item) {
const noteTags = item.tags?.slice(0, 3) || [];
const tags = [];
for (const tagName of noteTags) {
const tag = db.tags.tag(tagName);
if (!tag) continue;
tags.push(tag);
}
return tags;
}
const NoteItem = ({
item,
isTrash,
dateBy = "dateCreated",
noOpen = false
}) => {
date,
color,
notebooks,
reminder,
tags,
attachmentsCount,
noOpen
}: NoteItemProps) => {
const currentEditingNote = useEditorStore(
(state) => state.currentEditingNote
);
const { colors } = useThemeColors();
const compactMode = useIsCompactModeEnabled(item);
const attachmentCount = db.attachments?.ofNote(item.id, "all")?.length || 0;
const compactMode = useIsCompactModeEnabled(
(item as TrashItem).itemType || item.type
);
const _update = useRelationStore((state) => state.updater);
// eslint-disable-next-line react-hooks/exhaustive-deps
const notebooks = React.useMemo(() => getNotebook(item), [item, _update]);
const reminders = db.relations.from(item, "reminder");
const reminder = getUpcomingReminder(reminders);
const noteColor = ColorValues[item.color?.toLowerCase()];
const tags = getTags(item);
const primaryColors =
currentEditingNote === item.id ? colors.selected : colors.primary;
@@ -134,44 +103,45 @@ const NoteItem = ({
flexWrap: "wrap"
}}
>
{notebooks?.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
tooltipText={item.title}
key={item.id}
height={25}
icon={item.type === "topic" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
marginRight: 0
}}
style={{
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
Notebook.navigate(item);
{notebooks?.items
?.filter(
(item) =>
item.id !== useNavigationStore.getState().focusedRouteId
)
.map((item) => (
<Button
title={
item.title.length > 25
? item.title.slice(0, 25) + "..."
: item.title
}
}}
/>
))}
tooltipText={item.title}
key={item.id}
height={25}
icon={item.type === "notebook" ? "bookmark" : "book-outline"}
type="grayBg"
fontSize={SIZE.xs}
iconSize={SIZE.sm}
textStyle={{
marginRight: 0
}}
style={{
borderRadius: 5,
marginRight: 5,
borderWidth: 0.5,
borderColor: primaryColors.border,
paddingHorizontal: 6,
marginBottom: 5
}}
onPress={() => {
NotebookScreen.navigate(item, true);
}}
/>
))}
<ReminderTime
reminder={reminder}
color={noteColor}
color={color?.colorCode}
onPress={() => {
Properties.present(reminder);
}}
@@ -185,9 +155,7 @@ const NoteItem = ({
{compactMode ? (
<Paragraph
numberOfLines={1}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
color={color?.colorCode || primaryColors.heading}
style={{
flexWrap: "wrap"
}}
@@ -198,9 +166,7 @@ const NoteItem = ({
) : (
<Heading
numberOfLines={1}
color={
ColorValues[item.color?.toLowerCase()] || primaryColors.heading
}
color={color?.colorCode || primaryColors.heading}
style={{
flexWrap: "wrap"
}}
@@ -253,13 +219,11 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={
Date.now() - item[dateBy] < 60000 ? 2000 : 60000
}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
{attachmentCount > 0 ? (
{attachmentsCount > 0 ? (
<View
style={{
flexDirection: "row",
@@ -276,7 +240,7 @@ const NoteItem = ({
color={colors.secondary.paragraph}
size={SIZE.xs}
>
{attachmentCount}
{attachmentsCount}
</Paragraph>
</View>
) : null}
@@ -289,10 +253,7 @@ const NoteItem = ({
style={{
marginRight: 6
}}
color={
ColorValues[item.color?.toLowerCase()] ||
primaryColors.accent
}
color={color?.colorCode || primaryColors.accent}
/>
) : null}
@@ -321,10 +282,10 @@ const NoteItem = ({
) : null}
{!isTrash && !compactMode && tags
? tags.map((item) =>
? tags.items?.map((item) =>
item.id ? (
<Button
title={"#" + item.alias}
title={"#" + item.title}
key={item.id}
height={23}
type="gray"
@@ -338,9 +299,9 @@ const NoteItem = ({
paddingHorizontal: 6,
marginRight: 4,
zIndex: 10,
maxWidth: tags.length > 1 ? 130 : null
maxWidth: tags.items?.length > 1 ? 130 : null
}}
onPress={() => navigateToTag(item)}
onPress={() => TaggedNotes.navigate(item, true)}
/>
) : null
)
@@ -368,7 +329,8 @@ const NoteItem = ({
marginRight: 6
}}
>
{item.itemType[0].toUpperCase() + item.itemType.slice(1)}
{(item as TrashItem).itemType[0].toUpperCase() +
(item as TrashItem).itemType.slice(1)}
</Paragraph>
</>
)}
@@ -424,8 +386,8 @@ const NoteItem = ({
color: colors.secondary.paragraph,
marginRight: 6
}}
time={item[dateBy]}
updateFrequency={Date.now() - item[dateBy] < 60000 ? 2000 : 60000}
time={date}
updateFrequency={Date.now() - date < 60000 ? 2000 : 60000}
/>
</>
) : null}
@@ -435,7 +397,7 @@ const NoteItem = ({
color={primaryColors.paragraph}
name="dots-horizontal"
size={SIZE.xl}
onPress={() => !noOpen && showActionSheet(item, isTrash)}
onPress={() => !noOpen && Properties.present(item)}
customStyle={{
justifyContent: "center",
height: 35,

View File

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

View File

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

View File

@@ -17,41 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { BaseTrashItem, Notebook } from "@notesnook/core";
import React from "react";
import { NotebookItem } from ".";
import { TopicNotes } from "../../../screens/notes/topic-notes";
import { ToastEvent } from "../../../services/event-manager";
import { db } from "../../../common/database";
import NotebookScreen from "../../../screens/notebook";
import { ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useTrashStore } from "../../../stores/use-trash-store";
import { db } from "../../../common/database";
import { presentDialog } from "../../dialog/functions";
import SelectionWrapper from "../selection-wrapper";
const navigateToNotebook = (item, canGoBack) => {
if (!item) return;
Navigation.navigate(
{
title: item.title,
name: "Notebook",
id: item.id,
type: "notebook"
},
{
title: item.title,
item: item,
canGoBack
}
);
};
export const openNotebookTopic = (item) => {
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
const isTrash = item.type === "trash";
const { selectedItemsList, setSelectedItem, selectionMode, clearSelection } =
useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode) {
setSelectedItem(item);
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
@@ -59,15 +42,15 @@ export const openNotebookTopic = (item) => {
if (isTrash) {
presentDialog({
title: `Restore ${item.itemType}`,
paragraph: `Restore or delete ${item.itemType} forever`,
title: `Restore notebook`,
paragraph: `Restore or delete notebook forever`,
positiveText: "Restore",
negativeText: "Delete",
positivePress: async () => {
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Restore successful",
type: "success"
});
@@ -75,8 +58,8 @@ export const openNotebookTopic = (item) => {
onClose: async () => {
await db.trash.delete(item.id);
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
useSelectionStore.getState().setSelectionMode(undefined);
ToastManager.show({
heading: "Permanently deleted items",
type: "success",
context: "local"
@@ -85,29 +68,30 @@ export const openNotebookTopic = (item) => {
});
return;
}
if (item.type === "topic") {
TopicNotes.navigate(item, true);
} else {
navigateToNotebook(item, true);
}
NotebookScreen.navigate(item, true);
};
type NotebookWrapperProps = {
item: Notebook | BaseTrashItem<Notebook>;
totalNotes: number;
date: number;
index: number;
};
export const NotebookWrapper = React.memo(
function NotebookWrapper({ item, index, dateBy, totalNotes }) {
function NotebookWrapper({
item,
index,
date,
totalNotes
}: NotebookWrapperProps) {
const isTrash = item.type === "trash";
return (
<SelectionWrapper
pinned={item.pinned}
index={index}
onPress={() => openNotebookTopic(item)}
height={item.type === "topic" ? 80 : 110}
item={item}
>
<SelectionWrapper onPress={() => openNotebook(item)} item={item}>
<NotebookItem
isTopic={item.type === "topic"}
item={item}
dateBy={dateBy}
date={date}
index={index}
isTrash={isTrash}
totalNotes={totalNotes}
@@ -117,17 +101,8 @@ export const NotebookWrapper = React.memo(
},
(prev, next) => {
if (prev.totalNotes !== next.totalNotes) return false;
if (prev.item.title !== next.item.title) return false;
if (prev.dateBy !== next.dateBy) {
return false;
}
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
return false;
}
if (prev.date !== next.date) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
return true;
}

View File

@@ -16,20 +16,21 @@ 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 { Reminder } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import type { Reminder } from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import ReminderSheet from "../../sheets/reminder";
import { IconButton } from "../../ui/icon-button";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ReminderSheet from "../../sheets/reminder";
import { ReminderTime } from "../../ui/reminder-time";
const ReminderItem = React.memo(
({
@@ -43,17 +44,23 @@ const ReminderItem = React.memo(
}) => {
const { colors } = useThemeColors();
const openReminder = () => {
const {
selectedItemsList,
setSelectedItem,
selectionMode,
clearSelection
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
}
ReminderSheet.present(item, undefined, isSheet);
};
return (
<SelectionWrapper
//@ts-ignore
index={index}
height={100}
onPress={openReminder}
item={item}
isSheet={isSheet}
>
<SelectionWrapper onPress={openReminder} item={item} isSheet={isSheet}>
<View
style={{
flexShrink: 1,
@@ -131,7 +138,11 @@ const ReminderItem = React.memo(
height: 30
}}
>
<Icon name="reload" size={SIZE.md} color={colors.primary.accent} />
<Icon
name="reload"
size={SIZE.md}
color={colors.primary.accent}
/>
<Paragraph
size={SIZE.xs}
color={colors.secondary.paragraph}

View File

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

View File

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

View File

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

View File

@@ -17,44 +17,58 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Tag } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { TaggedNotes } from "../../../screens/notes/tagged";
import { useThemeColors } from "@notesnook/theme";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { Properties } from "../../properties";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import SelectionWrapper from "../selection-wrapper";
const TagItem = React.memo(
({ item, index }) => {
const { colors, isDark } = useThemeColors();
({
item,
index,
totalNotes
}: {
item: Tag;
index: number;
totalNotes: number;
}) => {
const { colors } = useThemeColors();
const onPress = () => {
const {
selectedItemsList,
setSelectedItem,
selectionMode,
clearSelection
} = useSelectionStore.getState();
if (selectedItemsList.length > 0 && selectionMode === item.type) {
setSelectedItem(item.id);
return;
} else {
clearSelection();
}
TaggedNotes.navigate(item, true);
};
return (
<PressableButton
<SelectionWrapper
onPress={onPress}
selectedColor={colors.secondary.background}
item={item}
testID={notesnook.ids.tag.get(index)}
alpha={!isDark ? -0.02 : 0.02}
opacity={1}
customStyle={{
paddingHorizontal: 12,
flexDirection: "row",
paddingVertical: 12,
alignItems: "center",
width: "100%",
justifyContent: "space-between"
}}
>
<View
style={{
maxWidth: "92%"
flexGrow: 1,
flexShrink: 1
}}
>
<Heading size={SIZE.md}>
@@ -66,7 +80,7 @@ const TagItem = React.memo(
>
#
</Heading>
{item.alias}
{item.title}
</Heading>
<Paragraph
color={colors.secondary.paragraph}
@@ -75,10 +89,10 @@ const TagItem = React.memo(
marginTop: 5
}}
>
{item && item.noteIds.length && item.noteIds.length > 1
? item.noteIds.length + " notes"
: item.noteIds.length === 1
? item.noteIds.length + " note"
{totalNotes > 1
? totalNotes + " notes"
: totalNotes === 1
? totalNotes + " note"
: null}
</Paragraph>
</View>
@@ -99,14 +113,11 @@ const TagItem = React.memo(
alignItems: "center"
}}
/>
</PressableButton>
</SelectionWrapper>
);
},
(prev, next) => {
if (prev.item?.dateEdited !== next.item?.dateEdited) {
return false;
}
if (JSON.stringify(prev.item) !== JSON.stringify(next.item)) {
if (prev.item?.dateModified !== next.item?.dateModified) {
return false;
}

View File

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

View File

@@ -1,167 +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 { ActivityIndicator, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export const Empty = React.memo(
function Empty({
loading = true,
placeholderData,
headerProps,
type,
screen
}) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholderData.type || type,
screen === "Notes" ? "notes" : null
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={
ColorValues[headerProps.color?.toLowerCase()]
? headerProps.color
: "accent"
}
tip={tip || { text: placeholderData.paragraph }}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholderData.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholderData.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholderData.action}
buttonType={{
text:
colors.static[headerProps.color] || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholderData.heading}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholderData.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={
ColorValues[headerProps.color?.toLowerCase()] ||
colors.primary.accent
}
/>
</View>
</>
)}
</View>
);
},
(prev, next) => {
if (prev.loading === next.loading) return true;
return false;
}
);
/**
* Make a tips manager.
* The tip manager stores many tips. Each tip has following values
* 1. Text
* 2. contexts: An array of context strings. // Places where the tip can be shown
* 3. Button if any.
* 4. Image/Gif asset.
*
* Tip manager adds the following methods -> get(context). Returns a random tip for the following context.
*
* Tips can be shown in a sheet or in a list. For sheets, GeneralSheet can be used to
* render tips.
*
* Where can the tips be shown and how?
* 1. When transitioning, show tips in a sheet. Make sure its useful
* 2. Replace placeholders with tips.
* 3. Show tips in editor placeholder.
* 4. Show tips between list items?
*
* Tooltips.
* Small tooltips can be shown in initial render first time.
* Especially for items that are not shown on blank page. Should be
* in places where it makes sense and does not interrupt the user.
*
* Can also be shown when first time entering a screen that
* has something that the user might not know of. Like sorting and side menu.
*
* Todo:
* 1. Make a tip manager.
* 2. Make a list of tips.
* 3. Add images for those tips.
* 4. Show tips
*/

View File

@@ -0,0 +1,140 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import { ActivityIndicator, useWindowDimensions, View } from "react-native";
import { notesnook } from "../../../e2e/test.ids";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { TTip, useTip } from "../../services/tip-manager";
import { useSettingStore } from "../../stores/use-setting-store";
import { SIZE } from "../../utils/size";
import { Tip } from "../tip";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
export type PlaceholderData = {
title: string;
paragraph: string;
button?: string;
action?: () => void;
loading?: string;
type?: string;
};
type EmptyListProps = {
loading?: boolean;
placeholder?: PlaceholderData;
title?: string;
color?: string;
dataType: string;
screen?: string;
};
export const Empty = React.memo(function Empty({
loading = true,
placeholder,
title,
color,
dataType,
screen
}: EmptyListProps) {
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
const { height } = useWindowDimensions();
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const tip = useTip(
screen === "Notes" && introCompleted
? "first-note"
: placeholder?.type || ((dataType + "s") as any),
screen === "Notes" ? "notes" : "list"
);
return (
<View
style={[
{
height: height - (140 + insets.top),
width: "80%",
justifyContent: "center",
alignSelf: "center"
}
]}
>
{!loading ? (
<>
<Tip
color={color}
tip={
screen !== "Search"
? tip || ({ text: placeholder?.paragraph } as TTip)
: ({ text: placeholder?.paragraph } as TTip)
}
style={{
backgroundColor: "transparent",
paddingHorizontal: 0
}}
/>
{placeholder?.button && (
<Button
testID={notesnook.buttons.add}
type="grayAccent"
title={placeholder?.button}
iconPosition="right"
icon="arrow-right"
onPress={placeholder?.action}
buttonType={{
text: color || colors.primary.accent
}}
style={{
alignSelf: "flex-start",
borderRadius: 5,
height: 40
}}
/>
)}
</>
) : (
<>
<View
style={{
alignSelf: "center",
alignItems: "flex-start",
width: "100%"
}}
>
<Heading>{placeholder?.title}</Heading>
<Paragraph size={SIZE.sm} textBreakStrategy="balanced">
{placeholder?.loading}
</Paragraph>
<Seperator />
<ActivityIndicator
size={SIZE.lg}
color={color || colors.primary.accent}
/>
</View>
</>
)}
</View>
);
});

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ const MergeConflicts = () => {
if (editorController.current?.note?.id === note.id) {
// reload the note in editor
eSendEvent(eOnLoadNote, {
...editorController.current?.note,
item: editorController.current?.note,
forced: true
});
}
@@ -324,10 +324,12 @@ const MergeConflicts = () => {
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictPrimary", {
...note,
content: {
...content.current,
isPreview: true
item: {
...note,
content: {
...content.current,
isPreview: true
}
}
});
}}
@@ -359,8 +361,10 @@ const MergeConflicts = () => {
if (!note) return;
await sleep(300);
eSendEvent(eOnLoadNote + ":conflictSecondary", {
...note,
content: { ...content.current.conflicted, isPreview: true }
item: {
...note,
content: { ...content.current.conflicted, isPreview: true }
}
});
}}
/>

View File

@@ -17,26 +17,29 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import Editor from "../../screens/editor";
import EditorOverlay from "../../screens/editor/loading";
import { editorController } from "../../screens/editor/tiptap/utils";
import { eSendEvent, ToastEvent } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useEditorStore } from "../../stores/use-editor-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { useTrashStore } from "../../stores/use-trash-store";
import { eCloseSheet, eOnLoadNote } from "../../utils/events";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import DialogHeader from "../dialog/dialog-header";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
/**
*
* @param {any} param0
* @returns
*/
export default function NotePreview({ session, content, note }) {
const { colors } = useThemeColors();
const editorId = ":noteHistory";
@@ -46,7 +49,7 @@ export default function NotePreview({ session, content, note }) {
await db.trash.restore(note.id);
Navigation.queueRoutesForUpdate();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
ToastManager.show({
heading: "Restore successful",
type: "success"
});
@@ -57,7 +60,7 @@ export default function NotePreview({ session, content, note }) {
if (useEditorStore.getState()?.currentEditingNote === session?.noteId) {
if (editorController.current?.note) {
eSendEvent(eOnLoadNote, {
...editorController.current?.note,
item: editorController.current?.note,
forced: true
});
}
@@ -66,7 +69,7 @@ export default function NotePreview({ session, content, note }) {
eSendEvent(eCloseSheet);
Navigation.queueRoutesForUpdate();
ToastEvent.show({
ToastManager.show({
heading: "Note restored successfully",
type: "success"
});
@@ -83,7 +86,7 @@ export default function NotePreview({ session, content, note }) {
await db.trash.delete(note.id);
useTrashStore.getState().setTrash();
useSelectionStore.getState().setSelectionMode(false);
ToastEvent.show({
ToastManager.show({
heading: "Permanently deleted items",
type: "success",
context: "local"
@@ -117,10 +120,12 @@ export default function NotePreview({ session, content, note }) {
onLoad={async () => {
const _note = note || db.notes.note(session?.noteId)?.data;
eSendEvent(eOnLoadNote + editorId, {
..._note,
content: {
...content,
isPreview: true
item: {
..._note,
content: {
...content,
isPreview: true
}
}
});
}}

View File

@@ -25,7 +25,7 @@ import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
@@ -517,7 +517,7 @@ export const PricingPlans = ({
try {
if (!(await getPromo(value as string)))
throw new Error("Error applying promo code");
ToastEvent.show({
ToastManager.show({
heading: "Discount applied!",
type: "success",
context: "local"
@@ -525,7 +525,7 @@ export const PricingPlans = ({
setBuying(false);
} catch (e) {
setBuying(false);
ToastEvent.show({
ToastManager.show({
heading: "Promo code invalid or expired",
message: (e as Error).message,
type: "error",

View File

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

View File

@@ -17,7 +17,10 @@ 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, { useState } from "react";
import { DefaultColors } from "@notesnook/core/dist/collections/colors";
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
@@ -25,45 +28,56 @@ import { db } from "../../common/database";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { ColorValues } from "../../utils/colors";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { PressableButton } from "../ui/pressable";
import { useThemeColors } from "@notesnook/theme";
export const ColorTags = ({ item }) => {
export const ColorTags = ({ item }: { item: Note }) => {
const { colors } = useThemeColors();
const [note, setNote] = useState(item);
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const changeColor = async (color) => {
if (note.color === color.name) {
await db.notes.note(note.id).uncolor();
} else {
await db.notes.note(note.id).color(color.name);
const updater = useRelationStore((state) => state.updater);
const getColorInfo = async (colorCode: string) => {
const dbColor = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", colorCode)])
);
let isLinked = false;
if (dbColor) {
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
if (hasRelation) {
isLinked = true;
}
}
let _note = db.notes.note(note.id).data;
setNote({ ..._note });
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
return {
linked: isLinked,
item: dbColor
};
};
const _renderColor = (c) => {
const color = {
name: c,
value: ColorValues[c?.toLowerCase()]
};
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
const color = DefaultColors[name];
const [colorInfo, setColorInfo] = useState<{
linked: boolean;
item: Color | undefined;
}>();
useEffect(() => {
getColorInfo(color).then((info) => setColorInfo(info));
}, [color]);
return (
<PressableButton
type="accent"
accentColor={colors.static[color.name?.toLowerCase()]}
accentColor={color}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(c)}
key={color.value}
onPress={() => changeColor(color)}
testID={notesnook.ids.dialogs.actionsheet.color(name)}
key={color}
onPress={() => changeColor(name)}
customStyle={{
width: 30,
height: 30,
@@ -73,13 +87,40 @@ export const ColorTags = ({ item }) => {
marginRight: isTablet ? 10 : undefined
}}
>
{note.color?.toLowerCase() === color.name ? (
{colorInfo?.linked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
const changeColor = async (color: string) => {
const colorInfo = await getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = await db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor as unknown as ItemReference, item);
}
}
useRelationStore.getState().update();
setColorNotes();
Navigation.queueRoutesForUpdate();
eSendEvent(refreshNotesPage);
};
return (
<View
style={{
@@ -92,7 +133,9 @@ export const ColorTags = ({ item }) => {
justifyContent: isTablet ? "center" : "space-between"
}}
>
{Object.keys(ColorValues).map(_renderColor)}
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
return <ColorItem key={name} name={name} />;
})}
</View>
);
};

View File

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

View File

@@ -16,14 +16,13 @@ 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, { useRef } from "react";
import { Dimensions, Platform, View, useWindowDimensions } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { presentSheet } from "../../services/event-manager";
import SearchService from "../../services/search";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import { SIZE } from "../../utils/size";
import SheetProvider from "../sheet-provider";
@@ -34,7 +33,8 @@ import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { Synced } from "./synced";
import { Tags, TagStrip } from "./tags";
import { TagStrip, Tags } from "./tags";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
return (
@@ -52,7 +52,6 @@ const Line = ({ top = 6, bottom = 6 }) => {
export const Properties = ({ close = () => {}, item, buttons = [] }) => {
const { colors } = useThemeColors();
const alias = item.alias || item.title;
const isColor = !!ColorValues[item.title];
if (!item || !item.id) {
return (
@@ -90,9 +89,13 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
#
</Heading>
) : null}
{alias}
{item.title}
</Heading>
{item.type === "notebook" && item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}
{item.type === "note" ? (
<TagStrip close={close} item={item} />
) : null}
@@ -117,22 +120,21 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
<Line bottom={0} />
{item.type === "note" ? <Tags close={close} item={item} /> : null}
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
{item.type === "note" ? (
<View
style={{
paddingHorizontal: 12
}}
>
<Notebooks note={item} close={close} />
</View>
) : null}
<Items
item={item}
buttons={buttons}
close={() => {
close();
setTimeout(() => {
SearchService.updateAndSearch();
}, 1000);
}}
/>
@@ -152,7 +154,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
);
};
Properties.present = (item, buttons = [], isSheet) => {
Properties.present = async (item, buttons = [], isSheet) => {
if (!item) return;
let type = item?.type;
let props = [];
@@ -164,7 +166,7 @@ Properties.present = (item, buttons = [], isSheet) => {
break;
case "note":
android = Platform.OS === "android" ? ["pin-to-notifications"] : [];
props[0] = db.notes.note(item.id).data;
props[0] = await db.notes.note(item.id);
props.push([
"notebooks",
"add-reminder",
@@ -189,33 +191,34 @@ Properties.present = (item, buttons = [], isSheet) => {
]);
break;
case "notebook":
props[0] = db.notebooks.notebook(item.id).data;
props[0] = await db.notebooks.notebook(item.id);
props.push([
"edit-notebook",
"pin",
"add-shortcut",
"trash",
"default-notebook"
]);
break;
case "topic":
props[0] = db.notebooks
.notebook(item.notebookId)
.topics.topic(item.id)._topic;
props.push([
"move-notes",
"edit-topic",
"add-shortcut",
"trash",
"default-topic"
"default-notebook",
"add-notebook"
]);
break;
// case "topic":
// props[0] = db.notebooks
// .notebook(item.notebookId)
// .topics.topic(item.id)._topic;
// props.push([
// "move-notes",
// "edit-topic",
// "add-shortcut",
// "trash",
// "default-topic"
// ]);
// break;
case "tag":
props[0] = db.tags.tag(item.id);
props[0] = await db.tags.tag(item.id);
props.push(["add-shortcut", "trash", "rename-tag"]);
break;
case "reminder": {
props[0] = db.reminders.reminder(item.id);
props[0] = await db.reminders.reminder(item.id);
props.push(["edit-reminder", "trash", "disable-reminder"]);
break;
}

View File

@@ -28,6 +28,7 @@ import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import { PressableButton } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
export const Items = ({ item, buttons, close }) => {
const { colors } = useThemeColors();
const dimensions = useSettingStore((state) => state.dimensions);
@@ -177,17 +178,37 @@ export const Items = ({ item, buttons, close }) => {
"export",
"lock-unlock"
];
if (!shouldShrink) {
topBarItemsList.push("publish");
}
const topBarItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) > -1
);
const topBarItems = data
.filter((item) => topBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
topBarItemsList.indexOf(a.id) > topBarItemsList.indexOf(b.id) ? 1 : -1
);
const bottomGridItems = data.filter(
(item) => topBarItemsList.indexOf(item.id) === -1
);
const bottomBarItemsList = [
"notebooks",
"add-reminder",
"pin-to-notifications",
"duplicate",
"read-only",
"local-only",
"history",
"reminders",
"attachments",
"trash"
];
const bottomGridItems = data
.filter((item) => bottomBarItemsList.indexOf(item.id) > -1)
.sort((a, b) =>
bottomBarItemsList.indexOf(a.id) > bottomBarItemsList.indexOf(b.id)
? 1
: -1
);
const topBarItemWidth =
(width - (topBarItems.length * 10 + 14)) / topBarItems.length;

View File

@@ -17,76 +17,46 @@ 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 { ScrollView, View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import Notebook from "../../screens/notebook";
import { TopicNotes } from "../../screens/notes/topic-notes";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useNotebookStore } from "../../stores/use-notebook-store";
import { useThemeColors } from "@notesnook/theme";
import NotebookScreen from "../../screens/notebook";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eClearEditor } from "../../utils/events";
import { SIZE } from "../../utils/size";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import { eClearEditor } from "../../utils/events";
import { PressableButton } from "../ui/pressable";
export default function Notebooks({ note, close, full }) {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => state.notebooks);
function getNotebooks(item) {
let filteredNotebooks = [];
const relations = db.relations.to(note, "notebook");
filteredNotebooks.push(
...relations.map((notebook) => ({
...notebook,
topics: []
}))
);
if (!item.notebooks || item.notebooks.length < 1) return filteredNotebooks;
for (let notebookReference of item.notebooks) {
let notebook = {
...(notebooks.find((item) => item.id === notebookReference.id) || {})
};
if (notebook.id) {
notebook.topics = notebook.topics.filter((topic) => {
return notebookReference.topics.findIndex((t) => t === topic.id) > -1;
});
const index = filteredNotebooks.findIndex(
(item) => item.id === notebook.id
);
if (index > -1) {
filteredNotebooks[index].topics = notebook.topics;
} else {
filteredNotebooks.push(notebook);
}
}
}
return filteredNotebooks;
async function getNotebooks() {
let filteredNotebooks = await db.relations.to(note, "notebook").resolve();
return filteredNotebooks || [];
}
const noteNotebooks = getNotebooks(note);
const [noteNotebooks, setNoteNotebooks] = useState([]);
useEffect(() => {
getNotebooks().then((notebooks) => setNoteNotebooks(notebooks));
});
const navigateNotebook = (id) => {
let item = db.notebooks.notebook(id)?.data;
const navigateNotebook = async (id) => {
let item = await db.notebooks.notebook(id);
if (!item) return;
Notebook.navigate(item, true);
NotebookScreen.navigate(item, true);
};
const navigateTopic = (id, notebookId) => {
let item = db.notebooks.notebook(notebookId)?.topics?.topic(id)?._topic;
if (!item) return;
TopicNotes.navigate(item, true);
};
const renderItem = (item) => (
<View
<PressableButton
key={item.id}
style={{
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
type={full ? "transparent" : "grayBg"}
customStyle={{
justifyContent: "flex-start",
paddingHorizontal: 12,
flexDirection: "row",
@@ -94,10 +64,7 @@ export default function Notebooks({ note, close, full }) {
flexShrink: 1,
flexGrow: 1,
padding: 6,
borderWidth: full ? 0 : 1,
borderColor: colors.primary.background,
borderRadius: 10,
backgroundColor: full ? "transparent" : colors.secondary.background,
minHeight: 42
}}
>
@@ -115,65 +82,10 @@ export default function Notebooks({ note, close, full }) {
maxWidth: "50%"
}}
size={SIZE.sm}
onPress={() => {
navigateNotebook(item.id);
eSendEvent(eClearEditor);
close();
}}
>
{item.title}
</Heading>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={{
flexDirection: "row",
marginLeft: 8,
borderLeftColor: colors.primary.hover,
borderLeftWidth: 1,
paddingLeft: 8
}}
>
{item.topics.map((topic) => (
<Button
key={topic.id}
onPress={() => {
navigateTopic(topic.id, item.id);
eSendEvent(eClearEditor);
close();
}}
onLongPress={async () => {
await db.notes.removeFromNotebook(
{
id: item.id,
topic: topic.id
},
note
);
useNotebookStore.getState().setNotebooks();
Navigation.queueRoutesForUpdate();
ToastEvent.show({
heading: "Note removed from topic",
context: "local",
type: "success"
});
}}
title={topic.title}
type="gray"
height={30}
fontSize={SIZE.xs}
icon="bookmark-outline"
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
))}
<View style={{ width: 10 }} />
</ScrollView>
</View>
</PressableButton>
);
return noteNotebooks.length === 0 ? null : (
@@ -190,7 +102,7 @@ export default function Notebooks({ note, close, full }) {
{noteNotebooks.length > 1 && !full ? (
<Button
title={`See all linked notebooks`}
title={`View all linked notebooks`}
fontSize={SIZE.xs}
style={{
alignSelf: "flex-end",

View File

@@ -27,6 +27,7 @@ import { sleep } from "../../utils/time";
import ManageTagsSheet from "../sheets/manage-tags";
import { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -47,7 +48,7 @@ export const Tags = ({ item, close }) => {
>
<Button
onPress={async () => {
ManageTagsSheet.present([item]);
ManageTagsSheet.present([item.id]);
}}
buttonType={{
text: colors.primary.accent

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,365 +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, { useCallback, useEffect } from "react";
import { BackHandler, Platform, View } from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import SearchService from "../../services/search";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { useThemeColors } from "@notesnook/theme";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import ManageTagsSheet from "../sheets/manage-tags";
export const SelectionHeader = React.memo(() => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setSelectionMode = useSelectionStore((state) => state.setSelectionMode);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const currentScreen = useNavigationStore((state) => state.currentScreen);
const screen = currentScreen.name;
const insets = useGlobalSafeAreaInsets();
SearchService.prepareSearch?.();
const allItems = SearchService.getSearchInformation()?.get() || [];
const allSelected = allItems.length === selectedItemsList.length;
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
}, [selectionMode]);
const addToFavorite = async () => {
if (selectedItemsList.length > 0) {
selectedItemsList.forEach((item) => {
db.notes.note(item.id).favorite();
});
Navigation.queueRoutesForUpdate();
clearSelection();
}
};
const restoreItem = async () => {
if (selectedItemsList.length > 0) {
let noteIds = [];
selectedItemsList.forEach((item) => {
noteIds.push(item.id);
});
await db.trash.restore(...noteIds);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastEvent.show({
heading: "Restore successful",
type: "success"
});
}
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (selectedItemsList.length > 0) {
let noteIds = [];
selectedItemsList.forEach((item) => {
noteIds.push(item.id);
});
await db.trash.delete(...noteIds);
Navigation.queueRoutesForUpdate();
clearSelection();
}
},
positiveType: "errorShade"
});
};
const onBackPress = useCallback(() => {
clearSelection();
return true;
}, [clearSelection]);
useEffect(() => {
if (selectionMode) {
BackHandler.addEventListener("hardwareBackPress", onBackPress);
} else {
BackHandler.removeEventListener("hardwareBackPress", onBackPress);
}
}, [onBackPress, selectionMode]);
return !selectionMode ? null : (
<View
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.primary.background,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
borderRadius: 100
}}
>
<IconButton
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginRight: 10
}}
type="grayBg"
onPress={() => {
setSelectionMode(!selectionMode);
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
justifyContent: "center",
flexDirection: "row",
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<IconButton
onPress={async () => {
useSelectionStore
.getState()
.setAll(allSelected ? [] : [...allItems]);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="select-all"
size={SIZE.xl}
/>
{screen === "Trash" ||
screen === "Notebooks" ||
screen === "Reminders" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
//setSelectionMode(false);
await sleep(100);
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{screen === "TopicNotes" || screen === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
const currentScreen =
useNavigationStore.getState().currentScreen;
if (screen === "Notebook") {
for (const item of selectedItemsList) {
await db.relations.unlink(
{ type: "notebook", id: currentScreen.id },
item
);
}
} else {
await db.notes.removeFromNotebook(
{
id: currentScreen.notebookId,
topic: currentScreen.id
},
...selectedItemsList.map((item) => item.id)
);
}
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from ${
screen === "Notebook" ? "notebook" : "topic"
}`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{screen === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{screen === "Trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={() => {
deleteItems();
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{screen === "Trash" ? (
<>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null}
</View>
</View>
);
});
SelectionHeader.displayName = "SelectionHeader";
export default SelectionHeader;

View File

@@ -0,0 +1,363 @@
/*
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 { Item, ItemType, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect } from "react";
import {
BackHandler,
NativeEventSubscription,
Platform,
View
} from "react-native";
import { db } from "../../common/database";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import { ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useSelectionStore } from "../../stores/use-selection-store";
import { deleteItems } from "../../utils/functions";
import { tabBarRef } from "../../utils/global-refs";
import { updateNotebook } from "../../utils/notebooks";
import { SIZE } from "../../utils/size";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import MoveNoteSheet from "../sheets/add-to";
import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { IconButton } from "../ui/icon-button";
import Heading from "../ui/typography/heading";
import Animated, { FadeInUp } from "react-native-reanimated";
export const SelectionHeader = React.memo(
({
items,
type,
id,
renderedInRoute
}: {
items?: VirtualizedGrouping<Item>;
id?: string;
type?: ItemType;
renderedInRoute?: string;
}) => {
const { colors } = useThemeColors();
const selectionMode = useSelectionStore((state) => state.selectionMode);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const insets = useGlobalSafeAreaInsets();
const allSelected =
items?.ids?.filter((id) => typeof id === "string").length ===
selectedItemsList.length;
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
useEffect(() => {
if (selectionMode) {
tabBarRef.current?.lock();
} else {
tabBarRef.current?.unlock();
}
}, [selectionMode]);
const addToFavorite = async () => {
if (!selectedItemsList.length) return;
db.notes.favorite(true, ...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
};
const restoreItem = async () => {
if (!selectedItemsList.length) return;
await db.trash.restore(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
ToastManager.show({
heading: "Restore successful",
type: "success"
});
};
const deleteItem = async () => {
presentDialog({
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
paragraph: `Are you sure you want to delete ${
selectedItemsList.length > 1
? "these items permanently?"
: "this item permanently?"
}`,
positiveText: "Delete",
negativeText: "Cancel",
positivePress: async () => {
if (!selectedItemsList.length) return;
await db.trash.delete(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();
},
positiveType: "errorShade"
});
};
useEffect(() => {
const onBackPress = () => {
clearSelection();
return true;
};
let sub: NativeEventSubscription | undefined;
if (selectionMode) {
sub = BackHandler.addEventListener("hardwareBackPress", onBackPress);
}
return () => {
sub?.remove();
};
}, [clearSelection, selectionMode]);
return selectionMode !== type || focusedRouteId !== id ? null : (
<Animated.View
entering={FadeInUp}
style={{
width: "100%",
height: Platform.OS === "android" ? 50 + insets.top : 50,
paddingTop: Platform.OS === "android" ? insets.top : null,
backgroundColor: colors.primary.background,
justifyContent: "space-between",
alignItems: "center",
flexDirection: "row",
zIndex: 999,
paddingHorizontal: 12,
marginVertical: 10
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
borderRadius: 100
}}
>
<IconButton
customStyle={{
justifyContent: "center",
alignItems: "center",
height: 40,
width: 40,
borderRadius: 100,
marginRight: 10
}}
type="grayBg"
onPress={() => {
clearSelection();
}}
size={SIZE.xl}
color={colors.primary.icon}
name="close"
/>
<View
style={{
backgroundColor: colors.secondary.background,
height: 40,
borderRadius: 100,
paddingHorizontal: 16,
justifyContent: "center",
flexDirection: "row",
alignItems: "center"
}}
>
<Heading size={SIZE.md} color={colors.primary.accent}>
{selectedItemsList.length}
</Heading>
</View>
</View>
<View
style={{
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center"
}}
>
<IconButton
onPress={async () => {
useSelectionStore
.getState()
.setAll(
allSelected
? []
: [
...((items?.ids.filter(
(id) => typeof id !== "object"
) as string[]) || [])
]
);
}}
tooltipText="Select all"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={
allSelected ? colors.primary.accent : colors.primary.paragraph
}
name="select-all"
size={SIZE.xl}
/>
{type !== "note" ? null : (
<>
<IconButton
onPress={async () => {
await sleep(100);
ManageTagsSheet.present(selectedItemsList);
}}
customStyle={{
marginLeft: 10
}}
color={colors.primary.icon}
tooltipText="Manage tags"
tooltipPosition={4}
name="pound"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
ExportNotesSheet.present(selectedItemsList);
}}
tooltipText="Export"
tooltipPosition={4}
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
name="export"
size={SIZE.xl}
/>
<IconButton
onPress={async () => {
MoveNoteSheet.present();
}}
customStyle={{
marginLeft: 10
}}
tooltipText="Add to notebooks"
tooltipPosition={4}
color={colors.primary.paragraph}
name="plus"
size={SIZE.xl}
/>
</>
)}
{renderedInRoute === "Notebook" ? (
<IconButton
onPress={async () => {
if (selectedItemsList.length > 0) {
if (!id) return;
await db.notes.removeFromNotebook(id, ...selectedItemsList);
updateNotebook(id);
Navigation.queueRoutesForUpdate();
clearSelection();
}
}}
customStyle={{
marginLeft: 10
}}
tooltipText={`Remove from Notebook`}
tooltipPosition={4}
testID="select-minus"
color={colors.primary.paragraph}
name="minus"
size={SIZE.xl}
/>
) : null}
{focusedRouteId === "Favorites" ? (
<IconButton
onPress={addToFavorite}
customStyle={{
marginLeft: 10
}}
tooltipText="Remove from favorites"
tooltipPosition={4}
color={colors.primary.paragraph}
name="star-off"
size={SIZE.xl}
/>
) : null}
{type === "trash" ? null : (
<IconButton
customStyle={{
marginLeft: 10
}}
onPress={() => {
deleteItems(
undefined,
useSelectionStore.getState().selectionMode
);
}}
tooltipText="Move to trash"
tooltipPosition={1}
color={colors.primary.paragraph}
name="delete"
size={SIZE.xl}
/>
)}
{type === "trash" ? (
<>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={restoreItem}
name="delete-restore"
tooltipText="Restore"
tooltipPosition={4}
size={SIZE.xl - 3}
/>
<IconButton
customStyle={{
marginLeft: 10
}}
color={colors.primary.paragraph}
onPress={deleteItem}
tooltipText="Delete"
tooltipPosition={4}
name="delete"
size={SIZE.xl - 3}
/>
</>
) : null}
</View>
</Animated.View>
);
}
);
SelectionHeader.displayName = "SelectionHeader";
export default SelectionHeader;

View File

@@ -1,553 +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 { ToastEvent, 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) {
ToastEvent.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.notebook(toEdit.id).topics.add(...nextTopics);
this.close();
} else {
newNotebookId = await db.notebooks.add({
title: this.title,
description: this.description,
topics: prevTopics,
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,189 @@
/*
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";
import { getParentNotebookId } from "../../../utils/notebooks";
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();
const parent =
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
eSendEvent(eOnNotebookUpdated, parent);
if (notebook) {
setImmediate(() => {
console.log(parent, notebook.id);
eSendEvent(eOnNotebookUpdated, notebook.id);
});
}
if (!notebook) {
MoveNotes.present(await db.notebooks.notebook(id));
} 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;
}}
onLayout={() => {
setImmediate(() => {
titleInput?.current?.focus();
});
}}
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,
context?: string
) => {
presentSheet({
context: context,
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}
parentNotebook={parentNotebook}
close={close}
/>
)
});
};

View File

@@ -1,76 +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, { useCallback, useEffect, useRef, useState } from "react";
import { FlashList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { ListHeaderInputItem } from "./list-header-item.js";
export const FilteredList = ({
data,
itemType,
onAddItem,
hasHeaderSearch,
listRef,
...restProps
}) => {
const [filtered, setFiltered] = useState(data);
const query = useRef();
const onChangeText = useCallback(
(value) => {
query.current = value;
try {
if (!value) return setFiltered(data);
const results = db.lookup[itemType + "s"]([...data], value);
setFiltered(results);
} catch (e) {
console.warn(e.message);
}
},
[data, itemType]
);
const onSubmit = async (value) => {
return await onAddItem(value);
};
useEffect(() => {
onChangeText(query.current);
}, [data, onChangeText]);
return (
<FlashList
{...restProps}
data={filtered}
ref={listRef}
ListHeaderComponent={
hasHeaderSearch ? (
<ListHeaderInputItem
onSubmit={onSubmit}
onChangeText={onChangeText}
itemType={itemType}
testID={"list-input" + itemType}
placeholder={`Search or add a new ${itemType}`}
/>
) : null
}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
/>
);
};

View File

@@ -1,486 +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, { useCallback, useEffect, useMemo } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
eSendEvent,
presentSheet,
ToastEvent
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import SearchService from "../../../services/search";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { eOnTopicSheetUpdate } from "../../../utils/events";
import { useThemeColors } from "@notesnook/theme";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { presentDialog } from "../../dialog/functions";
import { Button } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph";
import { SelectionProvider } from "./context";
import { FilteredList } from "./filtered-list";
import { ListItem } from "./list-item";
import { useItemSelectionStore } from "./store";
import { useRelationStore } from "../../../stores/use-relation-store";
const MoveNoteSheet = ({ note, actionSheetRef }) => {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) =>
state.notebooks.filter((n) => n?.type === "notebook")
);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const setNotebooks = useNotebookStore((state) => state.setNotebooks);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const onAddNotebook = async (title) => {
if (!title || title.trim().length === 0) {
ToastEvent.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) {
ToastEvent.show({
heading: "Topic title is required",
type: "error",
context: "local"
});
return false;
}
await db.notebooks.notebook(item.id).topics.add(value);
setNotebooks();
return true;
},
[setNotebooks]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const getSelectedNotesCountInItem = React.useCallback(
(item) => {
switch (item.type) {
case "notebook": {
const notes = db.relations.from(item, "note");
if (notes.length === 0) return 0;
let count = 0;
selectedItemsList.forEach((item) =>
notes.findIndex((note) => note.id === item.id) > -1
? count++
: undefined
);
return count;
}
case "topic": {
const noteIds = db.notes?.topicReferences.get(item.id);
let count = 0;
selectedItemsList.forEach((item) =>
noteIds.indexOf(item.id) > -1 ? count++ : undefined
);
return count;
}
}
},
[selectedItemsList]
);
useEffect(() => {
resetItemState();
return () => {
useItemSelectionStore.getState().setMultiSelect(false);
useItemSelectionStore.getState().setItemState({});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const resetItemState = useCallback(
(state) => {
const itemState = {};
const notebooks = db.notebooks.all;
let count = 0;
for (let notebook of notebooks) {
itemState[notebook.id] = state
? state
: areAllSelectedItemsInNotebook(notebook, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(notebook, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[notebook.id] === "selected") {
count++;
}
for (let topic of notebook.topics) {
itemState[topic.id] = state
? state
: areAllSelectedItemsInTopic(topic, selectedItemsList) &&
getSelectedNotesCountInItem(topic, selectedItemsList)
? "selected"
: getSelectedNotesCountInItem(topic, selectedItemsList) > 0
? "intermediate"
: "deselected";
if (itemState[topic.id] === "selected") {
count++;
}
}
}
if (count > 1) {
useItemSelectionStore.getState().setMultiSelect(true);
} else {
useItemSelectionStore.getState().setMultiSelect(false);
}
useItemSelectionStore.getState().setItemState(itemState);
},
[getSelectedNotesCountInItem, selectedItemsList]
);
const getItemsForItem = (item) => {
switch (item.type) {
case "notebook":
return item.topics?.filter((t) => t.type === "topic");
}
};
function areAllSelectedItemsInNotebook(notebook, items) {
const notes = db.relations.from(notebook, "note");
if (notes.length === 0) return false;
return items.every((item) => {
return notes.find((note) => note.id === item.id);
});
}
function areAllSelectedItemsInTopic(topic, items) {
return items.every((item) => {
return db.notes.topicReferences.get(topic.id).indexOf(item.id) > -1;
});
}
const updateItemState = useCallback(function (item, state) {
const itemState = { ...useItemSelectionStore.getState().itemState };
const mergeState = {
[item.id]: state
};
useItemSelectionStore.getState().setItemState({
...itemState,
...mergeState
});
}, []);
const contextValue = useMemo(
() => ({
toggleSelection: (item) => {
const itemState = useItemSelectionStore.getState().itemState;
if (itemState[item.id] === "selected") {
updateItemState(item, "deselected");
} else {
updateItemState(item, "selected");
}
},
deselect: (item) => {
updateItemState(item, "deselected");
},
select: (item) => {
updateItemState(item, "selected");
},
deselectAll: (state) => {
resetItemState(state);
}
}),
[resetItemState, updateItemState]
);
const getItemFromId = (id) => {
for (const nb of notebooks) {
if (nb.id === id) return nb;
for (const tp of nb.topics) {
if (tp.id === id) return tp;
}
}
};
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList.map((n) => n.id);
const itemState = useItemSelectionStore.getState().itemState;
for (const id in itemState) {
const item = getItemFromId(id);
if (itemState[id] === "selected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.add(item, { id: noteId, type: "note" });
}
} else {
await db.notes.addToNotebook(
{
topic: item.id,
id: item.notebookId,
rebuildCache: true
},
...noteIds
);
}
} else if (itemState[id] === "deselected") {
if (item.type === "notebook") {
for (let noteId of noteIds) {
await db.relations.unlink(item, { id: noteId, type: "note" });
}
} else {
await db.notes.removeFromNotebook(
{
id: item.notebookId,
topic: item.id,
rebuildCache: true
},
...noteIds
);
}
}
}
Navigation.queueRoutesForUpdate();
setNotebooks();
eSendEvent(eOnTopicSheetUpdate);
SearchService.updateAndSearch();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};
return (
<>
<Dialog context="move_note" />
<View>
<TouchableOpacity
style={{
width: "100%",
height: "100%",
position: "absolute"
}}
onPress={() => {
Keyboard.dismiss();
}}
/>
<View
style={{
paddingHorizontal: 12,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "flex-start"
}}
>
<DialogHeader
style={{
minHeight: 10,
flexShrink: 1
}}
title="Select notebooks"
paragraph={
!multiSelect
? "Long press to enable multi-select."
: "Select topics you want to add note(s) to."
}
/>
<Button
height={35}
style={{
borderRadius: 100,
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title="Save"
type={"accent"}
onPress={onSave}
/>
</View>
<View
style={{
paddingHorizontal: 12
}}
>
<Button
title="Reset selection"
height={30}
style={{
alignSelf: "flex-start",
paddingHorizontal: 0,
width: "100%",
marginTop: 6
}}
type="grayAccent"
onPress={() => {
resetItemState();
}}
/>
</View>
<SelectionProvider value={contextValue}>
<View
style={{
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85,
height: 50 * (notebooks.length + 2)
}}
>
<FilteredList
ListEmptyComponent={
notebooks.length > 0 ? null : (
<View
style={{
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name="book-outline"
color={colors.primary.icon}
size={100}
/>
<Paragraph style={{ marginBottom: 10 }}>
You do not have any notebooks.
</Paragraph>
</View>
)
}
estimatedItemSize={50}
data={notebooks}
hasHeaderSearch={true}
renderItem={({ item, index }) => (
<ListItem
item={item}
key={item.id}
index={index}
hasNotes={getSelectedNotesCountInItem(item) > 0}
sheetRef={actionSheetRef}
infoText={
<>
{item.topics.length === 1
? item.topics.length + " topic"
: item.topics.length + " topics"}
</>
}
getListItems={getItemsForItem}
getSublistItemProps={(topic) => ({
hasNotes: getSelectedNotesCountInItem(topic) > 0,
style: {
marginBottom: 0,
height: 40
},
onPress: (item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
},
key: item.id,
type: "transparent"
})}
icon={(expanded) => ({
name: expanded ? "chevron-up" : "chevron-down",
color: expanded
? colors.primary.accent
: colors.primary.paragraph
})}
onScrollEnd={() => {
actionSheetRef.current?.handleChildScrollEnd();
}}
hasSubList={true}
hasHeaderSearch={false}
type="grayBg"
sublistItemType="topic"
onAddItem={(title) => {
return onAddTopic(title, item);
}}
onAddSublistItem={(item) => {
openAddTopicDialog(item);
}}
onPress={(item) => {
const itemState =
useItemSelectionStore.getState().itemState;
const currentState = itemState[item.id];
if (currentState !== "selected") {
resetItemState("deselected");
contextValue.select(item);
} else {
contextValue.deselect(item);
}
}}
/>
)}
itemType="notebook"
onAddItem={async (title) => {
return await onAddNotebook(title);
}}
ListFooterComponent={<View style={{ height: 20 }} />}
/>
</View>
</SelectionProvider>
</View>
</>
);
};
MoveNoteSheet.present = (note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -0,0 +1,255 @@
/*
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 { Note } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useCallback, useEffect } from "react";
import { Keyboard, TouchableOpacity, View } from "react-native";
import { ActionSheetRef, FlashList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { ItemSelection } from "../../../stores/item-selection-store";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { updateNotebook } from "../../../utils/notebooks";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import SheetProvider from "../../sheet-provider";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Paragraph from "../../ui/typography/paragraph";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
.to(
{
type: "note",
ids: items
},
"notebook"
)
.get();
const initialSelectionState: ItemSelection = {};
const notebookIds = [
...new Set(relations.map((relation) => relation.fromId))
];
for (const id of notebookIds) {
const all = items.every((noteId) => {
return (
relations.findIndex(
(relation) => relation.fromId === id && relation.toId === noteId
) > -1
);
});
if (all) {
initialSelectionState[id] = "selected";
} else {
initialSelectionState[id] = "intermediate";
}
}
useNotebookItemSelectionStore.setState({
initialState: initialSelectionState,
selection: { ...initialSelectionState },
multiSelect: relations.length > 1
});
}
const MoveNoteSheet = ({
note,
actionSheetRef
}: {
note: Note | undefined;
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const notebooks = useNotebookStore((state) => state.notebooks);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const multiSelect = useNotebookItemSelectionStore(
(state) => state.multiSelect
);
useEffect(() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
return () => {
useNotebookItemSelectionStore.setState({
initialState: {},
selection: {},
multiSelect: false,
canEnableMultiSelectMode: true
});
};
}, [note, selectedItemsList]);
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList;
const changedNotebooks = useNotebookItemSelectionStore.getState().selection;
for (const id in changedNotebooks) {
const item = await db.notebooks.notebook(id);
if (!item) continue;
if (changedNotebooks[id] === "selected") {
for (const id of noteIds) {
await db.relations.add(item, { id: id, type: "note" });
updateNotebook(item.id);
}
} else if (changedNotebooks[id] === "deselected") {
for (const id of noteIds) {
await db.relations.unlink(item, { id: id, type: "note" });
updateNotebook(item.id);
}
}
}
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
actionSheetRef.current?.hide();
};
const renderNotebook = useCallback(
({ item, index }: { item: string | number; index: number }) => (
<NotebookItem items={notebooks} id={item as string} index={index} />
),
[notebooks]
);
return (
<>
<Dialog context="move_note" />
<SheetProvider context="link-notebooks" />
<View>
<TouchableOpacity
style={{
width: "100%",
height: "100%",
position: "absolute"
}}
onPress={() => {
Keyboard.dismiss();
}}
/>
<View
style={{
paddingHorizontal: 12,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "flex-start"
}}
>
<DialogHeader
style={{
minHeight: 10,
flexShrink: 1
}}
title="Select notebooks"
paragraph={
!multiSelect
? "Long press to enable multi-select."
: "Select topics you want to add note(s) to."
}
/>
<View
style={{
flexDirection: "row",
columnGap: 10
}}
>
<IconButton
name="restore"
type="grayAccent"
onPress={() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
}}
customStyle={{
width: 40,
height: 40
}}
/>
<Button
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title="Save"
type={"accent"}
onPress={onSave}
/>
</View>
</View>
<View
style={{
paddingHorizontal: 0,
maxHeight: dimensions.height * 0.85,
height: dimensions.height * 0.85,
paddingTop: 6
}}
>
<FlashList
data={notebooks?.ids}
style={{
width: "100%"
}}
estimatedItemSize={50}
renderItem={renderNotebook}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
<Paragraph color={colors.primary.icon}>No notebooks</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
</View>
</>
);
};
MoveNoteSheet.present = (note?: Note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true
});
};
export default MoveNoteSheet;

View File

@@ -1,88 +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, { useRef, useState } from "react";
import { View } from "react-native";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { useThemeColors } from "@notesnook/theme";
export const ListHeaderInputItem = ({
onSubmit,
onChangeText,
placeholder,
testID
}) => {
const [focused, setFocused] = useState(false);
const { colors } = useThemeColors("sheet");
const [inputValue, setInputValue] = useState();
const inputRef = useRef();
return (
<View
style={{
width: "100%",
marginTop: 10,
marginBottom: 5
}}
>
<Input
fwdRef={inputRef}
onChangeText={(value) => {
setInputValue(value);
onChangeText?.(value);
}}
testID={testID}
blurOnSubmit={false}
onFocusInput={() => {
setFocused(true);
}}
onBlurInput={() => {
setFocused(false);
}}
button={{
icon: inputValue ? "plus" : "magnify",
color: focused ? colors.selected.icon : colors.secondary.icon,
onPress: async () => {
const result = await onSubmit(inputValue);
if (result) {
inputRef.current?.blur();
}
}
}}
placeholder={placeholder}
marginBottom={5}
/>
{inputValue ? (
<View
style={{
backgroundColor: colors.primary.shade,
padding: 5,
borderRadius: 5,
marginBottom: 10
}}
>
<Paragraph color={colors.primary.accent}>
Tap on + to add {`"${inputValue}"`}
</Paragraph>
</View>
) : null}
</View>
);
};

View File

@@ -1,293 +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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useSelectionContext } from "./context";
import { FilteredList } from "./filtered-list";
import { useItemSelectionStore } from "./store";
const SelectionIndicator = ({
item,
hasNotes,
selectItem,
onPress,
onChange
}) => {
const itemState = useItemSelectionStore((state) => state.itemState[item.id]);
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const isSelected = itemState === "selected";
const isIntermediate = itemState === "intermediate";
const isRemoved = !isSelected && hasNotes;
const { colors } = useThemeColors("sheet");
useEffect(() => {
onChange?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemState]);
return (
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
isRemoved
? colors.static.red
: isIntermediate || isSelected
? colors.selected.icon
: colors.primary.icon
}
onPress={() => {
if (multiSelect) return selectItem();
onPress?.(item);
}}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
testID={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
isRemoved
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: isIntermediate
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
);
};
export const ListItem = ({
item,
index,
icon,
infoText,
hasSubList,
onPress,
onScrollEnd,
getListItems,
style,
type,
sublistItemType,
onAddItem,
getSublistItemProps,
hasHeaderSearch,
onAddSublistItem,
hasNotes,
onChange,
sheetRef
}) => {
const { toggleSelection } = useSelectionContext();
const multiSelect = useItemSelectionStore((state) => state.multiSelect);
const [showSelectedIndicator, setShowSelectedIndicator] = useState(false);
const { colors } = useThemeColors("sheet");
const [expanded, setExpanded] = useState(false);
function selectItem() {
toggleSelection(item);
}
const getSelectedNotesCountInNotebookTopics = (item) => {
if (item.type === "topic") return;
let count = 0;
const noteIds = [];
for (let topic of item.topics) {
noteIds.push(...(db.notes?.topicReferences.get(topic.id) || []));
if (useItemSelectionStore.getState().itemState[topic.id] === "selected") {
count++;
}
}
useSelectionStore.getState().selectedItemsList.forEach((item) => {
if (noteIds.indexOf(item.id) > -1) {
count++;
}
});
return count;
};
useEffect(() => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
}, [item]);
const onChangeSubItem = () => {
setShowSelectedIndicator(getSelectedNotesCountInNotebookTopics(item) > 0);
};
return (
<View
style={{
overflow: "hidden",
marginBottom: 10,
...style
}}
>
<PressableButton
onPress={() => {
if (hasSubList) return setExpanded(!expanded);
if (multiSelect) return selectItem();
onPress?.(item);
}}
type={type}
onLongPress={() => {
useItemSelectionStore.getState().setMultiSelect(true);
selectItem();
}}
customStyle={{
height: style?.height || 50,
width: "100%",
alignItems: "flex-start"
}}
>
<View
style={{
width: "100%",
height: 50,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 12
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
<SelectionIndicator
hasNotes={hasNotes}
onPress={onPress}
item={item}
onChange={onChange}
selectItem={selectItem}
/>
<View>
{hasSubList && expanded ? (
<Heading size={SIZE.md}>{item.title}</Heading>
) : (
<Paragraph size={SIZE.sm}>{item.title}</Paragraph>
)}
{infoText ? (
<Paragraph size={SIZE.xs} color={colors.primary.icon}>
{infoText}
</Paragraph>
) : null}
</View>
</View>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{showSelectedIndicator ? (
<View
style={{
backgroundColor: colors.primary.accent,
width: 7,
height: 7,
borderRadius: 100,
marginRight: 12
}}
/>
) : null}
{onAddSublistItem ? (
<IconButton
name={"plus"}
testID="add-item-icon"
color={colors.primary.paragraph}
size={SIZE.xl}
onPress={() => {
onAddSublistItem(item);
}}
/>
) : null}
{icon ? (
<IconButton
name={icon(expanded).name}
color={icon(expanded).color}
size={icon(expanded).size || SIZE.xl}
onPress={
hasSubList
? () => setExpanded(!expanded)
: icon(expanded).onPress
}
/>
) : null}
</View>
</View>
</PressableButton>
{expanded && hasSubList ? (
<FilteredList
nestedScrollEnabled
data={getListItems(item)}
keyboardShouldPersistTaps="always"
keyboardDismissMode="none"
onMomentumScrollEnd={onScrollEnd}
style={{
width: "95%",
alignSelf: "flex-end",
maxHeight: 250
}}
estimatedItemSize={40}
itemType={sublistItemType}
hasHeaderSearch={hasHeaderSearch}
renderItem={({ item, index }) => (
<ListItem
item={item}
{...getSublistItemProps(item)}
index={index}
onChange={onChangeSubItem}
onScrollEnd={onScrollEnd}
/>
)}
onAddItem={onAddItem}
/>
) : null}
</View>
);
};

View File

@@ -0,0 +1,256 @@
/*
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 { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View, useWindowDimensions } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { useTotalNotes } from "../../../hooks/use-db-item";
import { useNotebook } from "../../../hooks/use-notebook";
import useNavigationStore from "../../../stores/use-navigation-store";
import { SIZE } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { PressableButton } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import {
useNotebookExpandedStore,
useNotebookItemSelectionStore
} from "./store";
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
export const NotebookItem = ({
id,
currentLevel = 0,
index,
parent,
items
}: {
id: string | number;
currentLevel?: number;
index: number;
parent?: NotebookParentProp;
items?: VirtualizedGrouping<Notebook>;
}) => {
const expanded = useNotebookExpandedStore((state) => state.expanded[id]);
const { nestedNotebooks, notebook: item } = useNotebook(id, items, expanded);
const { totalNotes: totalNotes, getTotalNotes } = useTotalNotes("notebook");
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
const { colors } = useThemeColors("sheet");
const selection = useNotebookItemSelectionStore((state) =>
item?.id ? state.selection[item?.id] : undefined
);
const isSelected = selection === "selected";
const isFocused = focusedRouteId === id;
const { fontScale } = useWindowDimensions();
useEffect(() => {
if (item?.id) {
getTotalNotes([item?.id]);
}
}, [getTotalNotes, item?.id]);
const onPress = () => {
if (!item) return;
const state = useNotebookItemSelectionStore.getState();
if (isSelected) {
state.markAs(
item,
!state.initialState[item?.id] ? undefined : "deselected"
);
return;
}
if (!state.multiSelect) {
const keys = Object.keys(state.selection);
const nextState: any = {};
for (const key in keys) {
nextState[key] = !state.initialState[key] ? undefined : "deselected";
}
console.log("Single item selection");
state.setSelection({
[item.id]: "selected",
...nextState
});
} else {
console.log("Multi item selection");
state.markAs(item, "selected");
}
};
return (
<View
style={{
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
width: "100%"
}}
>
<PressableButton
type={"transparent"}
onLongPress={() => {
if (!item) return;
useNotebookItemSelectionStore.setState({
multiSelect: true
});
const state = useNotebookItemSelectionStore.getState();
useNotebookItemSelectionStore
.getState()
.markAs(
item,
!isSelected
? "selected"
: !state.initialState[item?.id]
? undefined
: "deselected"
);
}}
testID={`add-to-notebook-item-${currentLevel}-${index}`}
onPress={onPress}
customStyle={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingLeft: 12,
paddingRight: 12,
borderRadius: 0,
height: 45
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{nestedNotebooks?.ids.length ? (
<IconButton
size={SIZE.xl}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
if (!item?.id) return;
useNotebookExpandedStore.getState().setExpanded(item?.id);
}}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 35,
height: 35
}}
name={expanded ? "chevron-down" : "chevron-right"}
/>
) : null}
<IconButton
size={SIZE.xl}
color={
isSelected
? colors.selected.icon
: selection === "deselected"
? colors.error.accent
: colors.primary.icon
}
onPress={onPress}
top={0}
left={0}
bottom={0}
right={0}
customStyle={{
width: 40,
height: 40
}}
name={
selection === "deselected"
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<Paragraph
color={
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
}
size={SIZE.sm}
>
{item?.title}
</Paragraph>
</View>
<View
style={{
flexDirection: "row",
columnGap: 10,
alignItems: "center"
}}
>
{item?.id && totalNotes?.(item?.id) ? (
<Paragraph size={SIZE.sm} color={colors.secondary.paragraph}>
{totalNotes(item?.id)}
</Paragraph>
) : null}
<IconButton
name="plus"
customStyle={{
width: 40 * fontScale,
height: 40 * fontScale
}}
testID={notesnook.ids.notebook.menu}
onPress={() => {
if (!item) return;
AddNotebookSheet.present(undefined, item, "link-notebooks");
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.primary.icon}
size={SIZE.xl}
/>
</View>
</PressableButton>
{!expanded
? null
: nestedNotebooks?.ids.map((id, index) => (
<NotebookItem
key={item?.id + "_" + id}
id={id}
index={index}
currentLevel={currentLevel + 1}
items={nestedNotebooks}
parent={{
parent: parent,
item: item
}}
/>
))}
</View>
);
};

View File

@@ -16,27 +16,24 @@ 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 create, { State } from "zustand";
import create from "zustand";
import { createItemSelectionStore } from "../../../stores/item-selection-store";
type SelectionItemState = Record<
string,
"intermediate" | "selected" | "deselected"
>;
export interface SelectionStore extends State {
itemState: SelectionItemState;
setItemState: (state: SelectionItemState) => void;
multiSelect: boolean;
setMultiSelect: (multiSelect: boolean) => void;
}
export const useItemSelectionStore = create<SelectionStore>((set) => ({
itemState: {},
setItemState: (itemState) => {
export const useNotebookExpandedStore = create<{
expanded: {
[id: string]: boolean;
};
setExpanded: (id: string) => void;
}>((set, get) => ({
expanded: {},
setExpanded(id: string) {
set({
itemState
expanded: {
...get().expanded,
[id]: !get().expanded[id]
}
});
},
multiSelect: false,
setMultiSelect: (multiSelect) => set({ multiSelect })
}
}));
export const useNotebookItemSelectionStore = createItemSelectionStore(true);

View File

@@ -24,7 +24,7 @@ import {
eSendEvent,
presentSheet,
PresentSheetOptions,
ToastEvent
ToastManager
} from "../../../services/event-manager";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
@@ -87,7 +87,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
);
eSendEvent(eUserLoggedIn);
close?.();
ToastEvent.show({
ToastManager.show({
heading: `Email changed`,
message: `Your account email has been updated to ${emailChangeData.current.email}`,
type: "success",
@@ -96,7 +96,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
}
} catch (e) {
setLoading(false);
ToastEvent.error(e as Error);
ToastManager.error(e as Error);
}
};

View File

@@ -17,20 +17,27 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useThemeColors } from "@notesnook/theme";
import React, { Fragment, useState } from "react";
import { Platform, StyleSheet, View, ActivityIndicator } from "react-native";
import { ActivityIndicator, Platform, StyleSheet, View } from "react-native";
import FileViewer from "react-native-file-viewer";
import Share from "react-native-share";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import { requestInAppReview } from "../../../services/app-review";
import {
PresentSheetOptions,
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Exporter from "../../../services/exporter";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../../stores/use-user-store";
import { getElevationStyle } from "../../../utils/elevation";
import { ph, pv, SIZE } from "../../../utils/size";
import { eCloseSheet } from "../../../utils/events";
import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
import DialogHeader from "../../dialog/dialog-header";
import { ProTag } from "../../premium/pro-tag";
@@ -40,37 +47,48 @@ import { PressableButton } from "../../ui/pressable";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { eSendEvent } from "../../../services/event-manager";
import { eCloseSheet } from "../../../utils/events";
import { requestInAppReview } from "../../../services/app-review";
const ExportNotesSheet = ({ notes, update }) => {
const ExportNotesSheet = ({
ids,
update
}: {
ids: string[];
update: ((props: PresentSheetOptions) => void) | undefined;
}) => {
const { colors } = useThemeColors();
const [exporting, setExporting] = useState(false);
const [complete, setComplete] = useState(false);
const [result, setResult] = useState({});
const [result, setResult] = useState<
| {
fileName: string;
filePath: string;
name: string;
type: string;
}
| undefined
>();
const [status, setStatus] = useState(null);
const premium = useUserStore((state) => state.premium);
const save = async (type) => {
const save = async (type: "pdf" | "txt" | "md" | "html") => {
if (exporting) return;
if (!PremiumService.get() && type !== "txt") return;
setExporting(true);
update({ disableClosing: true });
update?.({ disableClosing: true } as PresentSheetOptions);
setComplete(false);
let result;
if (notes.length > 1) {
result = await Exporter.bulkExport(notes, type, setStatus);
if (ids.length > 1) {
result = await Exporter.bulkExport(ids, type, setStatus);
} else {
result = await Exporter.exportNote(notes[0], type);
result = await Exporter.exportNote(ids[0], type);
await sleep(1000);
}
if (!result) {
update({ disableClosing: false });
update?.({ disableClosing: false } as PresentSheetOptions);
return setExporting(false);
}
setResult(result);
update({ disableClosing: false });
setResult(result as any);
update?.({ disableClosing: false } as PresentSheetOptions);
setComplete(true);
setExporting(false);
requestInAppReview();
@@ -141,9 +159,7 @@ const ExportNotesSheet = ({ notes, update }) => {
<DialogHeader
icon="export"
title={
notes.length > 1
? `Export ${notes.length} Notes`
: "Export Note"
ids.length > 1 ? `Export ${ids.length} Notes` : "Export Note"
}
paragraph={`All exports are saved in ${
Platform.OS === "android"
@@ -229,7 +245,7 @@ const ExportNotesSheet = ({ notes, update }) => {
<>
<ActivityIndicator />
<Paragraph>
{notes.length === 1
{ids.length === 1
? "Exporting note... Please wait"
: `Exporting notes${
status ? ` (${status})` : ``
@@ -254,8 +270,8 @@ const ExportNotesSheet = ({ notes, update }) => {
}}
color={colors.secondary.heading}
>
{notes.length > 1
? `${notes.length} Notes exported`
{ids.length > 1
? `${ids.length} Notes exported`
: "Note exported"}
</Heading>
<Paragraph
@@ -263,8 +279,8 @@ const ExportNotesSheet = ({ notes, update }) => {
textAlign: "center"
}}
>
Your {notes.length > 1 ? "notes are" : "note is"} exported
successfully as {result.fileName}
Your {ids.length > 1 ? "notes are" : "note is"} exported
successfully as {result?.fileName}
</Paragraph>
<Button
title="Open"
@@ -276,14 +292,15 @@ const ExportNotesSheet = ({ notes, update }) => {
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
eSendEvent(eCloseSheet);
await sleep(500);
FileViewer.open(result.filePath, {
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch((e) => {
console.log(e);
ToastEvent.show({
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${result.name} file.`,
type: "success",
@@ -302,6 +319,7 @@ const ExportNotesSheet = ({ notes, update }) => {
borderRadius: 100
}}
onPress={async () => {
if (!result?.filePath) return;
if (Platform.OS === "ios") {
Share.open({
url: result.filePath
@@ -311,7 +329,7 @@ const ExportNotesSheet = ({ notes, update }) => {
showOpenWithDialog: true,
showAppsSuggestions: true,
shareFile: true
}).catch(console.log);
} as any).catch(console.log);
}
}}
/>
@@ -326,7 +344,7 @@ const ExportNotesSheet = ({ notes, update }) => {
}}
onPress={async () => {
setComplete(false);
setResult(null);
setResult(undefined);
setExporting(false);
}}
/>
@@ -339,13 +357,11 @@ const ExportNotesSheet = ({ notes, update }) => {
);
};
ExportNotesSheet.present = (notes, allNotes) => {
ExportNotesSheet.present = async (ids?: string[], allNotes?: boolean) => {
const exportNoteIds = allNotes ? await db.notes.all?.ids() : ids || [];
presentSheet({
component: (ref, close, update) => (
<ExportNotesSheet
notes={allNotes ? db.notes.all : notes}
update={update}
/>
<ExportNotesSheet ids={exportNoteIds} update={update} />
)
});
};

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { View } from "react-native";
import FileViewer from "react-native-file-viewer";
import { ToastEvent } from "../../../services/event-manager";
import { ToastManager } from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
export const ShareComponent = ({ uri, name, padding }) => {
@@ -40,7 +40,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
showOpenWithDialog: true,
showAppsSuggestions: true
}).catch(() => {
ToastEvent.show({
ToastManager.show({
heading: "Cannot open",
message: `No application found to open ${name} file.`,
type: "success",

View File

@@ -22,7 +22,7 @@ import React, { useRef, useState } from "react";
import { Linking, Platform, Text, TextInput, View } from "react-native";
import { getVersion } from "react-native-device-info";
import { db } from "../../../common/database";
import { eSendEvent, ToastEvent } from "../../../services/event-manager";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import { useUserStore } from "../../../stores/use-user-store";
@@ -100,7 +100,7 @@ Logged in: ${user ? "yes" : "no"}`,
positiveText: "Copy link",
positivePress: () => {
Clipboard.setString(issue_url);
ToastEvent.show({
ToastManager.show({
heading: "Issue url copied!",
type: "success",
context: "global"
@@ -110,8 +110,8 @@ Logged in: ${user ? "yes" : "no"}`,
});
} catch (e) {
setLoading(false);
ToastEvent.show({
heading: "An error occurred",
ToastManager.show({
heading: "An error occured",
message: e.message,
type: "error"
});

View File

@@ -1,287 +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, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useTagStore } from "../../../stores/use-tag-store";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { IconButton } from "../../ui/icon-button";
const ManageTagsSheet = (props) => {
const { colors } = useThemeColors();
const [notes, setNotes] = useState(props.notes || []);
const allTags = useTagStore((state) => state.tags);
const [tags, setTags] = useState([]);
const [query, setQuery] = useState(null);
const inputRef = useRef();
const [focus, setFocus] = useState(false);
useEffect(() => {
sortTags();
}, [allTags, notes, query, sortTags]);
const sortTags = useCallback(() => {
let _tags = [...allTags];
_tags = _tags.filter((t) => t.type === "tag");
_tags = _tags.sort((a, b) => a.title.localeCompare(b.title));
if (query) {
_tags = db.lookup.tags(_tags, query);
}
const tagsMerged = [...notes.map((note) => note.tags || []).flat()];
if (!tagsMerged || !tagsMerged.length) {
setTags(_tags);
return;
}
let noteTags = [];
for (let tag of tagsMerged) {
let index = _tags.findIndex((t) => t.title === tag);
if (index !== -1) {
noteTags.push(_tags[index]);
_tags.splice(index, 1);
}
}
noteTags = noteTags.sort((a, b) => a.title.localeCompare(b.title));
let combinedTags = [...noteTags, ..._tags];
setTags(combinedTags);
}, [allTags, notes, query]);
useEffect(() => {
useTagStore.getState().setTags();
}, []);
const onSubmit = async () => {
let _query = query;
if (!_query || _query === "" || _query.trimStart().length == 0) {
ToastEvent.show({
heading: "Tag field is empty",
type: "error",
context: "local"
});
return;
}
let tag = _query;
setNotes(
notes.map((note) => ({
...note,
tags: note.tags ? [...note.tags, tag] : [tag]
}))
);
setQuery(null);
inputRef.current?.setNativeProps({
text: ""
});
try {
for (let note of notes) {
await db.notes.note(note.id).tag(tag);
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
} catch (e) {
ToastEvent.show({
heading: "Cannot add tag",
type: "error",
message: e.message,
context: "local"
});
}
Navigation.queueRoutesForUpdate();
};
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
minHeight: focus ? "100%" : "60%"
}}
>
<Input
button={{
icon: "magnify",
color: colors.primary.accent,
size: SIZE.lg
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(db.tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
}}
onSubmit={onSubmit}
placeholder="Search or add a tag"
/>
<ScrollView
overScrollMode="never"
scrollToOverflowEnabled={false}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
{query && query !== tags[0]?.title ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="selected"
>
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
{!allTags || allTags.length === 0 ? (
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.secondary.heading}>
#
</Heading>
<Paragraph
textBreakStrategy="balanced"
color={colors.secondary.paragraph}
>
You do not have any tags.
</Paragraph>
</View>
) : null}
{tags.map((item) => (
<TagItem
key={item.title}
tag={item}
notes={notes}
setNotes={setNotes}
/>
))}
</ScrollView>
</View>
);
};
ManageTagsSheet.present = (notes) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} notes={notes} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({ tag, notes, setNotes }) => {
const { colors } = useThemeColors();
const someNotesTagged = notes.some(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const allNotesTagged = notes.every(
(note) => note.tags?.indexOf(tag.title) !== -1
);
const onPress = async () => {
for (let note of notes) {
try {
if (someNotesTagged) {
await db.notes
.note(note.id)
.untag(note.tags[note.tags.indexOf(tag.title)]);
} else {
await db.notes.note(note.id).tag(tag.title);
}
} catch (e) {
console.error(e);
}
}
useTagStore.getState().setTags();
setNotes(notes.map((note) => db.notes.note(note.id).data));
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
};
return (
<PressableButton
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
}}
onPress={onPress}
type="gray"
>
<IconButton
size={22}
customStyle={{
marginRight: 5,
width: 23,
height: 23
}}
color={
someNotesTagged || allNotesTagged
? colors.selected.icon
: colors.primary.icon
}
testID={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
allNotesTagged
? "check-circle-outline"
: someNotesTagged
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<Paragraph size={SIZE.sm}>{"#" + tag.alias}</Paragraph>
</PressableButton>
);
};

View File

@@ -0,0 +1,423 @@
/*
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 { VirtualizedGrouping } from "@notesnook/core";
import { Tags } from "@notesnook/core/dist/collections/tags";
import { Tag } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from "react";
import { TextInput, View, useWindowDimensions } from "react-native";
import { ActionSheetRef, FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { ToastManager, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import {
ItemSelection,
createItemSelectionStore
} from "../../../stores/item-selection-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useTagStore } from "../../../stores/use-tag-store";
import { SIZE } from "../../../utils/size";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
.to(
{
type: "note",
ids: items
},
"tag"
)
.get();
const initialSelectionState: ItemSelection = {};
const tagId = [...new Set(relations.map((relation) => relation.fromId))];
for (const id of tagId) {
const all = items.every((noteId) => {
return (
relations.findIndex(
(relation) => relation.fromId === id && relation.toId === noteId
) > -1
);
});
if (all) {
initialSelectionState[id] = "selected";
} else {
initialSelectionState[id] = "intermediate";
}
}
return initialSelectionState;
}
const useTagItemSelection = createItemSelectionStore(true);
const ManageTagsSheet = (props: {
ids?: string[];
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const ids = useMemo(() => props.ids || [], [props.ids]);
const [tags, setTags] = useState<VirtualizedGrouping<Tag>>();
const [query, setQuery] = useState<string>();
const inputRef = useRef<TextInput>(null);
const [focus, setFocus] = useState(false);
const [queryExists, setQueryExists] = useState(false);
const dimensions = useWindowDimensions();
const refreshSelection = useCallback(() => {
updateInitialSelectionState(ids).then((selection) => {
useTagItemSelection.setState({
initialState: selection,
selection: { ...selection }
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ids, tags]);
const refreshTags = useCallback(() => {
if (query && query.trim() !== "") {
db.lookup
.tags(query)
.sorted()
.then((items) => {
setTags(items);
console.log("searched tags");
});
} else {
db.tags.all.sorted(db.settings.getGroupOptions("tags")).then((items) => {
console.log("items loaded tags", items.ids);
setTags(items);
});
}
}, [query]);
useEffect(() => {
refreshTags();
}, [refreshTags, query]);
useEffect(() => {
refreshSelection();
}, [refreshSelection]);
const checkQueryExists = (query: string) => {
db.tags.all
.find((v) => v.and([v(`title`, "==", query)]))
.then((exists) => setQueryExists(!!exists));
};
const onSubmit = async () => {
if (!query || query === "" || query.trimStart().length == 0) {
ToastManager.show({
heading: "Tag field is empty",
type: "error",
context: "local"
});
return;
}
const tag = query;
inputRef.current?.setNativeProps({
text: ""
});
try {
const exists = await db.tags.all.find((v) =>
v.and([v(`title`, "==", tag)])
);
const id = exists
? exists?.id
: await db.tags.add({
title: tag
});
if (id) {
for (const noteId of ids) {
await db.relations.add(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
}
}
useRelationStore.getState().update();
useTagStore.getState().setTags();
setQuery(undefined);
} catch (e) {
ToastManager.show({
heading: "Cannot add tag",
type: "error",
message: (e as Error).message,
context: "local"
});
}
Navigation.queueRoutesForUpdate();
};
const onPress = useCallback(
async (id: string) => {
for (const noteId of ids) {
try {
if (!id) return;
const isSelected =
useTagItemSelection.getState().initialState[id] === "selected";
if (isSelected) {
await db.relations.unlink(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
} else {
await db.relations.add(
{
id: id,
type: "tag"
},
{
id: noteId,
type: "note"
}
);
}
} catch (e) {
console.error(e);
}
}
useTagStore.getState().setTags();
useRelationStore.getState().update();
refreshTags();
setTimeout(() => {
Navigation.queueRoutesForUpdate();
}, 1);
refreshSelection();
},
[ids, refreshSelection, refreshTags]
);
const renderTag = useCallback(
({ item, index }: { item: string | number; index: number }) => (
<TagItem
tags={tags as VirtualizedGrouping<Tag>}
id={index}
onPress={onPress}
/>
),
[onPress, tags]
);
return (
<View
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: 12,
maxHeight: dimensions.height * 0.85
}}
>
<Input
button={{
icon: "magnify",
color: colors.primary.accent,
size: SIZE.lg,
onPress: () => {}
}}
testID="tag-input"
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(v) => {
setQuery(Tags.sanitize(v));
checkQueryExists(Tags.sanitize(v));
}}
onFocusInput={() => {
setFocus(true);
}}
onBlurInput={() => {
setFocus(false);
}}
onSubmit={() => {
onSubmit();
}}
placeholder="Search or add a tag"
/>
{query && !queryExists ? (
<PressableButton
key={"query_item"}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "space-between",
padding: 12
}}
onPress={onSubmit}
type="selected"
>
<Heading size={SIZE.sm} color={colors.selected.heading}>
Add {'"' + "#" + query + '"'}
</Heading>
<Icon name="plus" color={colors.selected.icon} size={SIZE.lg} />
</PressableButton>
) : null}
<FlatList
data={tags?.ids}
style={{
width: "100%"
}}
keyboardShouldPersistTaps
keyboardDismissMode="interactive"
keyExtractor={(item) => item + "_tag"}
renderItem={renderTag}
ListEmptyComponent={
<View
style={{
width: "100%",
height: 200,
justifyContent: "center",
alignItems: "center"
}}
>
<Heading size={50} color={colors.secondary.heading}>
#
</Heading>
<Paragraph
textBreakStrategy="balanced"
color={colors.secondary.paragraph}
>
You do not have any tags.
</Paragraph>
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
);
};
ManageTagsSheet.present = (ids?: string[]) => {
presentSheet({
component: (ref) => {
return <ManageTagsSheet actionSheetRef={ref} ids={ids} />;
}
});
};
export default ManageTagsSheet;
const TagItem = ({
id,
tags,
onPress
}: {
id: string | number;
tags: VirtualizedGrouping<Tag>;
onPress: (id: string) => void;
}) => {
const { colors } = useThemeColors();
const [tag] = useDBItem(id, "tag", tags);
const selection = useTagItemSelection((state) =>
tag?.id ? state.selection[tag?.id] : false
);
return !tag ? null : (
<PressableButton
key={tag?.id}
customStyle={{
flexDirection: "row",
marginVertical: 5,
justifyContent: "flex-start",
height: 40
}}
onPress={() => {
if (!tag) return;
onPress(tag.id);
}}
type="gray"
>
{!tag ? null : (
<Icon
size={22}
onPress={() => {
if (!tag) return;
onPress(tag.id);
}}
color={
selection === "selected" || selection === "intermediate"
? colors.selected.icon
: colors.primary.icon
}
style={{
marginRight: 6
}}
testID={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
name={
selection === "selected"
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
)}
{tag ? (
<Paragraph size={SIZE.sm}>{"#" + tag?.title}</Paragraph>
) : (
<View
style={{
width: 200,
height: 30,
borderRadius: 5
}}
/>
)}
</PressableButton>
);
};

View File

@@ -26,7 +26,7 @@ import BackupService from "../../../services/backup";
import {
eSendEvent,
presentSheet,
ToastEvent
ToastManager
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { useThemeColors } from "@notesnook/theme";
@@ -94,7 +94,7 @@ export default function Migrate() {
await sleep(1000);
const backupSaved = await BackupService.run(false, "local");
if (!backupSaved) {
ToastEvent.show({
ToastManager.show({
heading: "Migration failed",
message: "You must download a backup of your data before migrating.",
context: "local"

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ import React, { useRef, useState } from "react";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import { presentSheet, ToastEvent } from "../../../services/event-manager";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { useThemeColors } from "@notesnook/theme";
@@ -36,23 +36,28 @@ import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
import { Note } from "@notesnook/core";
const PublishNoteSheet = ({ note: item, update }) => {
const PublishNoteSheet = ({
note: item
}: {
note: Note;
close?: (ctx?: string) => void;
}) => {
const { colors } = useThemeColors();
const actionSheetRef = useRef();
const attachmentDownloads = useAttachmentStore((state) => state.downloading);
const downloading = attachmentDownloads[`monograph-${item.id}`];
const downloading = attachmentDownloads?.[`monograph-${item.id}`];
const [selfDestruct, setSelfDestruct] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [note, setNote] = useState(item);
const [note, setNote] = useState<Note | undefined>(item);
const [publishing, setPublishing] = useState(false);
const publishUrl =
note &&
`https://monograph.notesnook.com/${db?.monographs.monograph(note?.id)}`;
const isPublished = note && db?.monographs.isPublished(note?.id);
const pwdInput = useRef();
const passwordValue = useRef();
`https://monograph.notesnook.com/${db.monographs.monograph(note?.id)}`;
const isPublished = note && db.monographs.isPublished(note?.id);
const pwdInput = useRef(null);
const passwordValue = useRef<string>();
const publishNote = async () => {
if (publishing) return;
@@ -60,20 +65,20 @@ const PublishNoteSheet = ({ note: item, update }) => {
try {
if (note?.id) {
if (isLocked && !passwordValue) return;
if (isLocked && !passwordValue.current) return;
await db.monographs.publish(note.id, {
selfDestruct: selfDestruct,
password: isLocked && passwordValue.current
password: isLocked ? passwordValue.current : undefined
});
setNote(db.notes.note(note.id)?.data);
setNote(await db.notes.note(note.id));
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
requestInAppReview();
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: "Could not publish note",
message: e.message,
message: (e as Error).message,
type: "error",
context: "local"
});
@@ -82,7 +87,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
setPublishLoading(false);
};
const setPublishLoading = (value) => {
const setPublishLoading = (value: boolean) => {
setPublishing(value);
};
@@ -92,19 +97,18 @@ const PublishNoteSheet = ({ note: item, update }) => {
try {
if (note?.id) {
await db.monographs.unpublish(note.id);
setNote(db.notes.note(note.id)?.data);
setNote(await db.notes.note(note.id));
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
} catch (e) {
ToastEvent.show({
ToastManager.show({
heading: "Could not unpublish note",
message: e.message,
message: (e as Error).message,
type: "error",
context: "local"
});
}
actionSheetRef.current?.hide();
setPublishLoading(false);
};
@@ -172,10 +176,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
<Paragraph
onPress={async () => {
try {
await openLinkInBrowser(
publishUrl,
colors.primary.accent
);
await openLinkInBrowser(publishUrl);
} catch (e) {
console.error(e);
}
@@ -193,8 +194,8 @@ const PublishNoteSheet = ({ note: item, update }) => {
<IconButton
onPress={() => {
Clipboard.setString(publishUrl);
ToastEvent.show({
Clipboard.setString(publishUrl as string);
ToastManager.show({
heading: "Note publish url copied",
type: "success",
context: "local"
@@ -357,10 +358,7 @@ const PublishNoteSheet = ({ note: item, update }) => {
}}
onPress={async () => {
try {
await openLinkInBrowser(
"https://docs.notesnook.com/monographs/",
colors.primary.accent
);
await openLinkInBrowser("https://docs.notesnook.com/monographs/");
} catch (e) {
console.error(e);
}
@@ -372,15 +370,10 @@ const PublishNoteSheet = ({ note: item, update }) => {
);
};
PublishNoteSheet.present = (note) => {
PublishNoteSheet.present = (note: Note) => {
presentSheet({
component: (ref, close, update) => (
<PublishNoteSheet
actionSheetRef={ref}
close={close}
update={update}
note={note}
/>
<PublishNoteSheet close={close} note={note} />
)
});
};

View File

@@ -23,11 +23,10 @@ import { Platform, View } from "react-native";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
import Share from "react-native-share";
//import { LOGO_BASE64 } from '../../../assets/images/assets';
import {
eSubscribeEvent,
eUnSubscribeEvent,
ToastEvent
ToastManager
} from "../../../services/event-manager";
import { clearMessage } from "../../../services/message";
import SettingsService from "../../../services/settings";
@@ -75,7 +74,7 @@ class RecoveryKeySheet extends React.Component {
close = () => {
if (this.tapCount === 0) {
ToastEvent.show({
ToastManager.show({
heading: "Did you save recovery key?",
message: "Tap one more time to confirm.",
type: "success",
@@ -129,7 +128,7 @@ class RecoveryKeySheet extends React.Component {
path = await Storage.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
}
ToastEvent.show({
ToastManager.show({
heading: "Recovery key QR-Code saved",
message:
"QR-Code image has been saved to Gallery at " + path + fileName,
@@ -164,7 +163,7 @@ class RecoveryKeySheet extends React.Component {
path = path + fileName;
}
ToastEvent.show({
ToastManager.show({
heading: "Recovery key text file saved",
message: "Recovery key saved in text file.",
type: "success",
@@ -284,7 +283,7 @@ class RecoveryKeySheet extends React.Component {
<Button
onPress={() => {
Clipboard.setString(this.state.key);
ToastEvent.show({
ToastManager.show({
heading: "Recovery key copied!",
type: "success",
context: "local"

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
PresentSheetOptions,
ToastEvent,
ToastManager,
presentSheet
} from "../../../services/event-manager";
import { SIZE } from "../../../utils/size";
@@ -35,22 +35,22 @@ import DatePicker from "react-native-date-picker";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import Navigation from "../../../services/navigation";
import Notifications, { Reminder } from "../../../services/notifications";
import Notifications from "../../../services/notifications";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useRelationStore } from "../../../stores/use-relation-store";
import { NoteType } from "../../../utils/types";
import { Dialog } from "../../dialog";
import { ReminderTime } from "../../ui/reminder-time";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { ItemReference, Note, Reminder } from "@notesnook/core";
type ReminderSheetProps = {
actionSheetRef: RefObject<ActionSheetRef>;
close?: (ctx?: string) => void;
update?: (options: PresentSheetOptions) => void;
reminder?: Reminder;
reference?: { id: string; type: string };
reference?: ItemReference;
};
const ReminderModes =
@@ -113,9 +113,8 @@ export default function ReminderSheet({
>(reminder?.priority || SettingsService.get().reminderNotificationMode);
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference
? (db.notes?.note(reference.id)?.data as NoteType)
: null;
const referencedItem = reference ? (reference as Note) : null;
const title = useRef<string | undefined>(
reminder?.title || referencedItem?.title
);
@@ -195,19 +194,19 @@ export default function ReminderSheet({
date?.getTime() > Date.now() ? undefined : reminder?.snoozeUntil,
disabled: false
});
const _reminder = db.reminders?.reminder(reminderId);
if (!reminderId) return;
const _reminder = await db.reminders?.reminder(reminderId);
if (!_reminder) {
ToastEvent.show({
ToastManager.show({
heading: "Failed to add a new reminder",
context: "local"
});
}
if (reference) {
if (reference && _reminder) {
await db.relations?.add(reference, {
id: _reminder?.id as string,
type: _reminder?.type as string
type: _reminder?.type
});
}
Notifications.scheduleNotification(_reminder as Reminder);
@@ -215,7 +214,7 @@ export default function ReminderSheet({
useRelationStore.getState().update();
close?.();
} catch (e) {
ToastEvent.error(e as Error, undefined, "local");
ToastManager.error(e as Error, undefined, "local");
}
}
@@ -517,7 +516,7 @@ export default function ReminderSheet({
mode="date"
onConfirm={handleConfirm}
onCancel={hideDatePicker}
is24Hour={db.settings?.getTimeFormat() === "24-hour"}
is24Hour={db.settings.getTimeFormat() === "24-hour"}
date={date || new Date(Date.now())}
/>
@@ -614,7 +613,7 @@ export default function ReminderSheet({
ReminderSheet.present = (
reminder?: Reminder,
reference?: { id: string; type: string },
reference?: ItemReference,
isSheet?: boolean
) => {
presentSheet({

View File

@@ -17,25 +17,26 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getFormattedDate } from "@notesnook/common";
import { EVENTS } from "@notesnook/core/dist/common";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Platform, View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import RNFetchBlob from "react-native-blob-util";
import DocumentPicker, {
DocumentPickerResponse
} from "react-native-document-picker";
import DocumentPicker from "react-native-document-picker";
import * as ScopedStorage from "react-native-scoped-storage";
import { unzip } from "react-native-zip-archive";
import { db } from "../../../common/database";
import storage from "../../../common/database/storage";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
import {
ToastEvent,
ToastManager,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { initialize } from "../../../stores";
import { useThemeColors } from "@notesnook/theme";
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { Dialog } from "../../dialog";
@@ -46,9 +47,6 @@ import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
import SheetWrapper from "../../ui/sheet";
import Paragraph from "../../ui/typography/paragraph";
import { getFormattedDate } from "@notesnook/common";
import { unzip } from "react-native-zip-archive";
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
const RestoreDataSheet = () => {
const [visible, setVisible] = useState(false);
@@ -81,7 +79,7 @@ const RestoreDataSheet = () => {
}, [restoring]);
const showIsWorking = () => {
ToastEvent.show({
ToastManager.show({
heading: "Restoring Backup",
message: "Your backup data is being restored. please wait.",
type: "error",
@@ -239,7 +237,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
await db.backup.import(backup, password);
await db.initCollections();
initialize();
ToastEvent.show({
ToastManager.show({
heading: "Backup restored successfully.",
type: "success",
context: "global"
@@ -248,7 +246,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
};
const backupError = (e) => {
ToastEvent.show({
ToastManager.show({
heading: "Restore failed",
message:
e.message ||
@@ -305,7 +303,7 @@ const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
initialize();
setRestoring(false);
close();
ToastEvent.show({
ToastManager.show({
heading: "Backup restored successfully.",
type: "success",
context: "global"

View File

@@ -24,7 +24,7 @@ import { eSendEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useThemeColors } from "@notesnook/theme";
import { GROUP, SORT } from "../../../utils/constants";
import { refreshNotesPage } from "../../../utils/events";
import { eGroupOptionsUpdated, refreshNotesPage } from "../../../utils/events";
import { SIZE } from "../../../utils/size";
import { Button } from "../../ui/button";
import Seperator from "../../ui/seperator";
@@ -33,15 +33,16 @@ const Sort = ({ type, screen }) => {
const isTopicSheet = screen === "TopicSheet";
const { colors } = useThemeColors();
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(type)
db.settings.getGroupOptions(screen === "Notes" ? "home" : type + "s")
);
const updateGroupOptions = async (_groupOptions) => {
await db.settings.setGroupOptions(type, _groupOptions);
const groupType = screen === "Notes" ? "home" : type + "s";
console.log("updateGroupOptions for group", groupType, "in", screen);
await db.settings.setGroupOptions(groupType, _groupOptions);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen !== "TopicSheet") Navigation.queueRoutesForUpdate(screen);
eSendEvent("groupOptionsUpdate");
eSendEvent(eGroupOptionsUpdated, groupType);
eSendEvent(refreshNotesPage);
}, 1);
};

View File

@@ -17,22 +17,21 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import Navigation from "../../services/navigation";
import useNavigationStore from "../../stores/use-navigation-store";
import { useMenuStore } from "../../stores/use-menu-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { ColorValues } from "../../utils/colors";
import React, { useCallback, useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { normalize, SIZE } from "../../utils/size";
import { ColoredNotes } from "../../screens/notes/colored";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { SIZE, normalize } from "../../utils/size";
import { presentDialog } from "../dialog/functions";
import { PressableButton } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { ColoredNotes } from "../../screens/notes/colored";
import { useCallback } from "react";
import { Color } from "@notesnook/core";
export const ColorSection = React.memo(
function ColorSection() {
@@ -46,32 +45,33 @@ export const ColorSection = React.memo(
}
}, [loading, setColorNotes]);
return colorNotes.map((item, index) => {
let alias = db.colors.alias(item.id);
return (
<ColorItem key={item.id} alias={alias} item={item} index={index} />
);
return colorNotes.map((item) => {
return <ColorItem key={item.id} item={item} />;
});
},
() => true
);
const ColorItem = React.memo(
function ColorItem({ item, alias }) {
function ColorItem({ item }: { item: Color }) {
const { colors, isDark } = useThemeColors();
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const [headerTextState, setHeaderTextState] = useState(null);
alias = db.colors.alias(item.id) || "";
const [headerTextState, setHeaderTextState] = useState<{
id: string | undefined;
}>({
id: undefined
});
const isFocused = headerTextState?.id === item.id;
const onHeaderStateChange = useCallback(
(state) => {
(state: any) => {
setTimeout(() => {
let id = state.currentScreen?.id;
let id = state.focusedRouteId;
if (id === item.id) {
setHeaderTextState({ id: state.currentScreen.id });
} else {
if (headerTextState !== null) {
setHeaderTextState(null);
setHeaderTextState({ id: undefined });
}
}
}, 300);
@@ -80,13 +80,13 @@ const ColorItem = React.memo(
);
useEffect(() => {
let unsub = useNavigationStore.subscribe(onHeaderStateChange);
const remove = useNavigationStore.subscribe(onHeaderStateChange);
return () => {
unsub();
remove();
};
}, [headerTextState, onHeaderStateChange]);
const onPress = (item) => {
const onPress = (item: Color) => {
ColoredNotes.navigate(item, false);
setImmediate(() => {
@@ -99,11 +99,14 @@ const ColorItem = React.memo(
title: "Rename color",
input: true,
inputPlaceholder: "Enter name for this color",
defaultValue: alias,
defaultValue: item.title,
paragraph: "You are renaming the color " + item.title,
positivePress: async (value) => {
if (!value || value.trim().length === 0) return;
await db.colors.rename(item.id, value);
await db.colors.add({
id: item.id,
title: value
});
setColorNotes();
},
positiveText: "Rename"
@@ -112,11 +115,9 @@ const ColorItem = React.memo(
return (
<PressableButton
customColor={
headerTextState?.id === item.id ? "rgba(0,0,0,0.04)" : "transparent"
}
customColor={isFocused ? "rgba(0,0,0,0.04)" : "transparent"}
onLongPress={onLongPress}
customSelectedColor={ColorValues[item.title.toLowerCase()]}
customSelectedColor={item.colorCode}
customAlpha={!isDark ? -0.02 : 0.02}
customOpacity={0.12}
onPress={() => onPress(item)}
@@ -149,20 +150,20 @@ const ColorItem = React.memo(
style={{
width: SIZE.lg - 2,
height: SIZE.lg - 2,
backgroundColor: ColorValues[item.title.toLowerCase()],
backgroundColor: item.colorCode,
borderRadius: 100,
justifyContent: "center",
marginRight: 10
}}
/>
</View>
{headerTextState?.id === item.id ? (
{isFocused ? (
<Heading color={colors.selected.heading} size={SIZE.md}>
{alias.slice(0, 1).toUpperCase() + alias.slice(1)}
{item.title?.slice(0, 1).toUpperCase() + item.title.slice(1)}
</Heading>
) : (
<Paragraph color={colors.primary.paragraph} size={SIZE.md}>
{alias.slice(0, 1).toUpperCase() + alias.slice(1)}
{item.title?.slice(0, 1).toUpperCase() + item.title.slice(1)}
</Paragraph>
)}
</View>
@@ -171,8 +172,8 @@ const ColorItem = React.memo(
},
(prev, next) => {
if (!next.item) return false;
if (prev.item?.title !== next.item.title) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
if (prev.alias !== next.alias) return false;
if (prev.item?.id !== next.item?.id) return false;
return true;

View File

@@ -65,10 +65,7 @@ export const SideMenu = React.memo(
icon: "cog-outline",
close: true,
func: () => {
Navigation.navigate({
name: "Settings",
title: "Settings"
});
Navigation.navigate("Settings");
}
}
];

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { Dimensions, View } from "react-native";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ToggleSwitch from "toggle-switch-react-native";
import Navigation from "../../services/navigation";
@@ -34,20 +34,18 @@ export const MenuItem = React.memo(
function MenuItem({ item, index, testID, rightBtn }) {
const { colors } = useThemeColors();
const [headerTextState, setHeaderTextState] = useState(
useNavigationStore.getState().currentScreen
useNavigationStore.getState().focusedRouteId
);
const screenId = item.name.toLowerCase() + "_navigation";
let isFocused = headerTextState?.id === screenId;
let isFocused = headerTextState?.id === item.name;
const primaryColors = isFocused ? colors.selected : colors.primary;
const _onPress = () => {
if (item.func) {
item.func();
} else {
Navigation.navigate(
{ name: item.name, beta: item.isBeta },
{ canGoBack: false }
);
if (useNavigationStore.getState().currentRoute !== item.name) {
Navigation.push(item.name, { canGoBack: false, beta: item.isBeta });
}
}
if (item.close) {
setImmediate(() => {
@@ -59,9 +57,9 @@ export const MenuItem = React.memo(
const onHeaderStateChange = useCallback(
(state) => {
setTimeout(() => {
let id = state.currentScreen?.id;
if (id === screenId) {
setHeaderTextState({ id: state.currentScreen.id });
let id = state.focusedRouteId;
if (id === item.name) {
setHeaderTextState({ id: state.focusedRouteId });
} else {
if (headerTextState !== null) {
setHeaderTextState(null);
@@ -69,7 +67,7 @@ export const MenuItem = React.memo(
}
}, 300);
},
[headerTextState, screenId]
[headerTextState, item.name]
);
useEffect(() => {

View File

@@ -17,19 +17,19 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef, useState } from "react";
import { Notebook, Tag } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { FlatList, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import Notebook from "../../screens/notebook";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import { TaggedNotes } from "../../screens/notes/tagged";
import { TopicNotes } from "../../screens/notes/topic-notes";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNoteStore } from "../../stores/use-notes-store";
import { useThemeColors } from "@notesnook/theme";
import { db } from "../../common/database";
import { normalize, SIZE } from "../../utils/size";
import { SIZE, normalize } from "../../utils/size";
import { Properties } from "../properties";
import { Button } from "../ui/button";
import { Notice } from "../ui/notice";
@@ -38,7 +38,6 @@ import Seperator from "../ui/seperator";
import SheetWrapper from "../ui/sheet";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { useCallback } from "react";
export const TagsSection = React.memo(
function TagsSection() {
@@ -52,23 +51,18 @@ export const TagsSection = React.memo(
}
}, [loading, setMenuPins]);
const onPress = (item) => {
const onPress = (item: Notebook | Tag) => {
if (item.type === "notebook") {
Notebook.navigate(item);
NotebookScreen.navigate(item);
} else if (item.type === "tag") {
TaggedNotes.navigate(item);
} else {
TopicNotes.navigate(item);
}
setImmediate(() => {
Navigation.closeDrawer();
});
};
const renderItem = ({ item, index }) => {
let alias = item.alias || item.title;
return (
<PinItem item={item} index={index} alias={alias} onPress={onPress} />
);
const renderItem = ({ item }: { item: Notebook | Tag; index: number }) => {
return <PinItem item={item} onPress={onPress} />;
};
return (
@@ -102,15 +96,26 @@ export const TagsSection = React.memo(
);
export const PinItem = React.memo(
function PinItem({ item, onPress, placeholder, alias }) {
function PinItem({
item,
onPress,
isPlaceholder
}: {
item: Notebook | Tag;
onPress: (item: Notebook | Tag) => void;
isPlaceholder?: boolean;
}) {
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
alias = item?.alias || item?.title;
const [visible, setVisible] = useState(false);
const [headerTextState, setHeaderTextState] = useState(null);
const [headerTextState, setHeaderTextState] = useState<{
id?: string;
}>({});
const primaryColors =
headerTextState?.id === item.id ? colors.selected : colors.primary;
const isFocused = headerTextState?.id === item.id;
const color =
headerTextState?.id === item.id
? colors.selected.accent
@@ -118,16 +123,16 @@ export const PinItem = React.memo(
const fwdRef = useRef();
const onHeaderStateChange = useCallback(
(state) => {
(state: any) => {
setTimeout(() => {
let id = state.currentScreen?.id;
const id = state.focusedRouteId;
if (id === item.id) {
setHeaderTextState({
id: state.currentScreen.id
id
});
} else {
if (headerTextState !== null) {
setHeaderTextState(null);
setHeaderTextState({});
}
}
}, 300);
@@ -136,9 +141,9 @@ export const PinItem = React.memo(
);
useEffect(() => {
let unsub = useNavigationStore.subscribe(onHeaderStateChange);
const remove = useNavigationStore.subscribe(onHeaderStateChange);
return () => {
unsub();
remove();
};
}, [headerTextState, onHeaderStateChange]);
@@ -157,7 +162,6 @@ export const PinItem = React.memo(
}}
gestureEnabled={false}
fwdRef={fwdRef}
visible={true}
>
<Seperator />
<Button
@@ -177,9 +181,9 @@ export const PinItem = React.memo(
</SheetWrapper>
)}
<PressableButton
type={headerTextState?.id === item.id ? "selected" : "gray"}
type={isFocused ? "selected" : "gray"}
onLongPress={() => {
if (placeholder) return;
if (isPlaceholder) return;
Properties.present(item);
}}
onPress={() => onPress(item)}
@@ -234,7 +238,7 @@ export const PinItem = React.memo(
flex: 1
}}
>
{headerTextState?.id === item.id ? (
{isFocused ? (
<Heading
style={{
flexWrap: "wrap"
@@ -242,7 +246,7 @@ export const PinItem = React.memo(
color={primaryColors.heading}
size={SIZE.md}
>
{alias}
{item.title}
</Heading>
) : (
<Paragraph
@@ -250,7 +254,7 @@ export const PinItem = React.memo(
color={primaryColors.paragraph}
size={SIZE.md}
>
{alias}
{item.title}
</Paragraph>
)}
</View>
@@ -261,7 +265,7 @@ export const PinItem = React.memo(
},
(prev, next) => {
if (!next.item) return false;
if (prev.alias !== next.alias) return false;
if (prev.item.title !== next.item.title) return false;
if (prev.item?.dateModified !== next.item?.dateModified) return false;
if (prev.item?.id !== next.item?.id) return false;
return true;

View File

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

View File

@@ -143,7 +143,6 @@ export const Button = ({
{!title ? null : (
<Component
animated={false}
color={textColor as string}
size={fontSize}
numberOfLines={1}

View File

@@ -64,6 +64,7 @@ interface InputProps extends TextInputProps {
color: ColorValue;
onPress: () => void;
testID?: string;
size?: number;
};
buttons?: React.ReactNode;
onBlurInput?: () => void;

View File

@@ -27,8 +27,7 @@ import {
View,
ViewStyle
} from "react-native";
import Animated from "react-native-reanimated";
import { hexToRGBA, RGB_Linear_Shade } from "../../../utils/colors";
import { RGB_Linear_Shade, hexToRGBA } from "../../../utils/colors";
import { br } from "../../../utils/size";
export interface PressableButtonProps extends PressableProps {
customStyle?: ViewStyle;
@@ -41,7 +40,6 @@ export interface PressableButtonProps extends PressableProps {
customAlpha?: number;
customOpacity?: number;
fwdRef?: RefObject<View>;
animatedViewProps?: Animated.AnimateProps<View>;
hidden?: boolean;
}

View File

@@ -20,11 +20,11 @@ import { isReminderActive } from "@notesnook/core/dist/collections/reminders";
import React from "react";
import { ViewStyle } from "react-native";
import { Reminder } from "../../../services/notifications";
import { useThemeColors } from "@notesnook/theme";
import { SIZE } from "../../../utils/size";
import { Button, ButtonProps } from "../button";
import { getFormattedReminderTime } from "@notesnook/common";
import { Reminder } from "@notesnook/core/dist/types";
export const ReminderTime = ({
checkIsActive = true,

View File

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

View File

@@ -17,24 +17,16 @@ 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, { useMemo } from "react";
import { Platform, Text, TextProps, ViewStyle } from "react-native";
import Animated, {
ComplexAnimationBuilder,
Layout
} from "react-native-reanimated";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Platform, Text, TextProps, ViewStyle } from "react-native";
import { SIZE } from "../../../utils/size";
interface HeadingProps extends TextProps {
color?: string;
size?: number;
layout?: ComplexAnimationBuilder;
animated?: boolean;
extraBold?: boolean;
}
const AnimatedText = Animated.createAnimatedComponent(Text);
const extraBoldStyle = {
fontFamily: Platform.OS === "android" ? "OpenSans-Bold" : undefined,
fontWeight: Platform.OS === "ios" ? "800" : undefined
@@ -48,16 +40,13 @@ const Heading = ({
color,
size = SIZE.xl,
style,
animated,
extraBold,
...restProps
}: HeadingProps) => {
const { colors } = useThemeColors();
const Component = useMemo(() => (animated ? AnimatedText : Text), [animated]);
return (
<Component
layout={restProps.layout || Layout}
<Text
allowFontScaling={true}
{...restProps}
style={[
@@ -68,7 +57,7 @@ const Heading = ({
extraBold ? (extraBoldStyle as ViewStyle) : (boldStyle as ViewStyle),
style
]}
></Component>
></Text>
);
};

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