Compare commits

..

40 Commits

Author SHA1 Message Date
Abdullah Atta
b7bd06c9f3 Release v3.3.22 (#9952)
* web: fix file size is 0 error on attachment upload (#9951)

* web: bump version to 3.3.22
2026-06-06 09:48:03 +05:00
Abdullah Atta
766a8f28d4 web: bump version to 3.3.22 2026-06-06 09:22:20 +05:00
Abdullah Atta
86512b5ce7 web: fix file size is 0 error on attachment upload (#9951) 2026-06-06 09:10:29 +05:00
Ammar Ahmed
a820fcde53 editor: allow any width/height value in EmbedSizeOptions when inserting embed (#9925)
* editor: allow any width/height value in EmbedSizeOptions when inserting embeds

* editor: only accept valid url as value

* editor: allow embeding normal website urls

* editor: check url if embedSource=url

* editor: use defaultValue prop for width/height
2026-06-04 13:16:51 +05:00
01zulfi
5ddf1eddf5 web: validate max date when creating reminder (#9937)
* web: validate max date when creating reminder
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: skip max date validation on repeat mode in reminder dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-04 13:16:01 +05:00
Asad Ali
4237d2eb4a desktop: fix spell check issue for non-latin languages #9423 (#9443)
* desktop: fix spell check issue for non-latin languages #9423

Signed-off-by: Asad Ali <asadali.wolf@gmail.com>

* Delete apps/desktop/src/api/spell-checker.test.ts

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

* desktop: refactor

---------

Signed-off-by: Asad Ali <asadali.wolf@gmail.com>
Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-06-04 09:36:34 +05:00
Ammar Ahmed
f67cd503d6 Merge pull request #9922 from streetwriters/fix/400
mobile: hide toc button if no headings in editor
2026-06-04 08:54:08 +05:00
Ammar Ahmed
14039d6a1e Merge pull request #9921 from streetwriters/fix/401
Fix long tag name does not wrap properly in editor
2026-06-04 08:53:50 +05:00
Ammar Ahmed
d1fed9072e Merge pull request #9890 from kashaf-ansari-dev/fix-duplicate-note-message
mobile: add success message after note duplication
2026-06-04 08:52:31 +05:00
kashaf-ansari-dev
b003a3ffed mobile: reduce translation diff noise
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-03 14:17:33 +05:00
kashaf-ansari-dev
55e10e4755 mobile: add success message after note duplication
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-03 14:14:07 +05:00
Ammar Ahmed
7c1dd0f79a Merge pull request #9915 from streetwriters/fix/359-360
mobile: fix unable to edit/delete reminder when accessing reminders from note properties
2026-06-03 14:03:41 +05:00
Ammar Ahmed
229cf195a8 Merge pull request #9920 from streetwriters/fix/357
Fix screen jerks when opening add reminder screen from note
2026-06-03 14:02:33 +05:00
Ammar Ahmed
38da6ac08b Merge pull request #9918 from streetwriters/fix/318
Fix themes do not load properly on mobile in theme selector
2026-06-03 14:01:58 +05:00
Ammar Ahmed
597dc587cd Merge pull request #9919 from streetwriters/fix/317
Fix subscription not active opens a sheet that keeps loading if there is an error
2026-06-03 14:01:32 +05:00
Ammar Ahmed
5c76b92789 Merge pull request #9893 from kashaf-ansari-dev/fix-publish-password-validation
mobile: validate password field before publishing note
2026-06-03 14:01:09 +05:00
Abdullah Atta
8418d5fa9c Merge pull request #9886 from streetwriters/web/notebook-tag-group-options
web: allow notebook/tag/color level group&sort preferences
2026-06-03 13:35:06 +05:00
01zulfi
cd071f8f58 web: ensure full year range is visible in reminder dialog's day picker (#9928)
* web: ensure full year range is visible in reminder dialog's day picker
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: include boundary years in day picker's fluid year dropdown
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-03 11:52:36 +05:00
01zulfi
32ddc3fa40 web: refresh tag & notebook filtered lists on item rename (#9923)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 15:27:47 +05:00
01zulfi
68d07a7862 web: fix properties panel in focus mode (#9917)
* allow properties panel to open/close when app is in focus mode

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 13:06:50 +05:00
01zulfi
2aa5162fc9 web: set edited date as max date in note creation date edit's day picker (#9924)
* web: set edited date as max date in note creation date edit's day picker
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix creation date not being able to set to same as edited date
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 13:02:06 +05:00
01zulfi
cf5565d09d web: don't allow note expiry to be set beyond 1 year in the future (#9901)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-02 08:56:21 +05:00
Ammar Ahmed
f129e0c411 mobile: hide toc button if no headings in note 2026-06-01 13:18:13 +05:00
01zulfi
bb43d80e05 Merge pull request #9916 from streetwriters/release/3.3.21
web: bump version to 3.3.21
2026-06-01 12:52:57 +05:00
Ammar Ahmed
4e4d3866df mobile: limit tag to single line only 2026-06-01 12:43:41 +05:00
Ammar Ahmed
b2088ef782 mobile: fix long tag name does not wrap properly in editor 2026-06-01 12:27:37 +05:00
Ammar Ahmed
07db14f6bf mobile: fix screen jerks when opening add reminder screen from note 2026-06-01 11:53:00 +05:00
Ammar Ahmed
82d37701c5 mobile: fix gap in buttons 2026-06-01 11:09:29 +05:00
Ammar Ahmed
b48ab3c85d mobile: fix query error on fetching themes in theme-selector 2026-06-01 10:56:35 +05:00
Ammar Ahmed
bb7ff4f376 mobile: show error if no active subscription is found 2026-06-01 10:13:31 +05:00
Ammar Ahmed
43b35917ef mobile: fix subscription not active screen gets stuck on loading on iOS 2026-06-01 09:32:05 +05:00
Ammar Ahmed
bcfa34ca05 mobile: fix unable to edit/delete reminder when accessing reminders from note properties 2026-06-01 08:40:30 +05:00
01zulfi
bd3edc535a web: use getGroupOptionsById & setGroupOptionsById
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-25 15:04:17 +05:00
01zulfi
1dff7a121d core: fix group options by id tests
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-25 14:50:57 +05:00
Ammar Ahmed
3f3467e3a7 mobile: add support for sorting/grouping items based on notebook/tag/color id 2026-05-25 13:21:48 +05:00
Ammar Ahmed
39bb9afa0e core: add getGroupOptionsById/setGroupOptionsById 2026-05-25 13:21:00 +05:00
kashaf-ansari-dev
886e757715 mobile: removed redundant isLocked check for password field in publish form
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-05-25 11:21:55 +05:00
kashaf-ansari-dev
5c93645969 mobile: migrate publish inputs to FormInput with validation
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-05-23 11:03:00 +05:00
kashaf-ansari-dev
07defdfa78 mobile: validate password field before publishing note
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-05-22 17:15:38 +05:00
01zulfi
27b00d4ef9 web: allow notebook/tag level group&sort preferences
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-05-22 10:48:27 +05:00
49 changed files with 838 additions and 458 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.21",
"version": "3.3.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.21",
"version": "3.3.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.21",
"version": "3.3.22",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -84,35 +84,55 @@ const LANGUAGES: Record<string, string> = {
type Language = { code: string; name: string };
const LANGUAGE_REDIRECT_MAP: Record<string, string> = {
es: "es-MX",
"es-419": "es-MX",
"es-ES": "es-AR"
};
export const spellCheckerRouter = t.router({
isEnabled: t.procedure.query(() => config.isSpellCheckerEnabled),
languages: t.procedure.query(
() =>
<Language[]>(
globalThis.window?.webContents.session.availableSpellCheckerLanguages.map(
(code) => ({
code,
name: LANGUAGES[code]
})
)
)
),
enabledLanguages: t.procedure.query(
() =>
<Language[]>(
globalThis.window?.webContents.session
.getSpellCheckerLanguages()
.map((code) => ({
code,
name: LANGUAGES[code]
}))
)
),
setLanguages: t.procedure
.input(z.array(z.string()))
.mutation(({ input: languages }) =>
globalThis.window?.webContents.session.setSpellCheckerLanguages(languages)
),
languages: t.procedure.query(() => {
const available =
globalThis.window?.webContents.session.availableSpellCheckerLanguages ||
[];
return <Language[]>available
.map((code) => ({
code,
name: LANGUAGES[code] || code
}))
.sort((a, b) => a.name.localeCompare(b.name));
}),
enabledLanguages: t.procedure.query(() => {
const enabled =
globalThis.window?.webContents.session.getSpellCheckerLanguages() || [];
const available =
globalThis.window?.webContents.session.availableSpellCheckerLanguages ||
[];
const resolved = enabled
.map((code) => resolveLanguage(code, available))
.filter(Boolean) as string[];
return <Language[]>resolved.map((code) => ({
code,
name: LANGUAGES[code] || code
}));
}),
setLanguages: t.procedure.input(z.array(z.string())).mutation(({ input }) => {
const available =
globalThis.window?.webContents.session.availableSpellCheckerLanguages ||
[];
const resolved = input
.map((code) => resolveLanguage(code, available))
.filter(Boolean) as string[];
globalThis.window?.webContents.session.setSpellCheckerLanguages(resolved);
}),
toggle: t.procedure
.input(z.object({ enabled: z.boolean() }))
.mutation(({ input: { enabled } }) => {
@@ -128,3 +148,16 @@ export const spellCheckerRouter = t.router({
);
})
});
function resolveLanguage(code: string, available: string[]) {
if (LANGUAGE_REDIRECT_MAP[code]) {
const working = LANGUAGE_REDIRECT_MAP[code];
return available.includes(working) ? working : code;
}
const fallback = code.split("-")[0];
return available.includes(code)
? code
: available.includes(fallback)
? fallback
: undefined;
}

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import {
GroupHeader,
GroupingByIdKey,
GroupingKey,
GroupOptions,
ItemType
@@ -46,6 +47,8 @@ type SectionHeaderProps = {
screen?: RouteName;
groupOptions: GroupOptions;
group: GroupingKey;
groupId?: string;
type?: GroupingByIdKey;
onOpenJumpToDialog: () => void;
itemCount?: number;
};
@@ -62,7 +65,9 @@ export const SectionHeader = React.memo<
groupOptions,
group,
onOpenJumpToDialog,
itemCount
itemCount,
groupId,
type
}: SectionHeaderProps) {
const { colors } = useThemeColors();
const isCompactModeEnabled = useIsCompactModeEnabled(
@@ -143,8 +148,10 @@ export const SectionHeader = React.memo<
component: (
<Sort
screen={screen}
type={dataType}
dataType={dataType}
type={type}
group={group}
groupId={groupId}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}

View File

@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { GroupingKey, Item, VirtualizedGrouping } from "@notesnook/core";
import {
GroupingByIdKey,
GroupingKey,
Item,
VirtualizedGrouping
} from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import { LegendList, LegendListRenderItemProps } from "@legendapp/list";
import React, { useEffect, useRef } from "react";
@@ -55,6 +60,7 @@ type ListProps = {
placeholder?: PlaceholderData;
groupType: GroupingKey;
id?: string;
type?: GroupingByIdKey;
};
const onMomentumScrollEnd = () => {
@@ -74,7 +80,7 @@ export default function List(props: ListProps) {
props.dataType === "notebook" ||
notebooksListMode === "compact";
const groupOptions = useGroupOptions(props.groupType);
const groupOptions = useGroupOptions(props.groupType, props.id, props.type);
const _onRefresh = async () => {
Sync.run("global", false, "full", () => {
@@ -96,23 +102,27 @@ export default function List(props: ListProps) {
index={itemProps.index}
isSheet={props.isRenderedInActionSheet || false}
items={props.data}
groupId={props.id}
groupOptions={groupOptions}
group={props.groupType as GroupingKey}
renderedInRoute={props.renderedInRoute}
customAccentColor={props.customAccentColor}
dataType={props.dataType}
type={props.type}
scrollRef={scrollRef}
/>
);
},
[
groupOptions,
props.groupType,
props.customAccentColor,
props.data,
props.dataType,
props.isRenderedInActionSheet,
props.renderedInRoute
props.data,
props.id,
props.groupType,
props.renderedInRoute,
props.customAccentColor,
props.dataType,
groupOptions,
props.type
]
);

View File

@@ -26,6 +26,7 @@ import {
Color,
GroupHeader,
GroupOptions,
GroupingByIdKey,
GroupingKey,
HighlightedResult,
Item,
@@ -40,7 +41,7 @@ import {
} from "@notesnook/core";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import { getGroupOptions } from "../../hooks/use-group-options";
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
import { eSendEvent } from "../../services/event-manager";
import { RouteName } from "../../stores/use-navigation-store";
@@ -49,8 +50,8 @@ import { SectionHeader } from "../list-items/headers/section-header";
import { NoteWrapper } from "../list-items/note/wrapper";
import { NotebookWrapper } from "../list-items/notebook/wrapper";
import ReminderItem from "../list-items/reminder";
import TagItem from "../list-items/tag";
import { SearchResult } from "../list-items/search-result";
import TagItem from "../list-items/tag";
type ListItemWrapperProps<TItem = Item> = {
group: GroupingKey;
@@ -62,6 +63,8 @@ type ListItemWrapperProps<TItem = Item> = {
dataType: string;
scrollRef: any;
groupOptions: GroupOptions;
groupId?: string;
type?: GroupingByIdKey;
};
export function ListItemWrapper(props: ListItemWrapperProps) {
@@ -183,6 +186,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
index={index}
dataType={item.type}
group={group}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -220,6 +225,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
index={index}
dataType={item.type}
group={group}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -250,6 +257,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
group={group}
dataType={item.type}
color={props.customAccentColor}
type={props.type}
groupId={props.groupId}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
@@ -276,6 +285,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
index={index}
group={group}
dataType={item.type}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
@@ -303,6 +314,8 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
index={index}
group={group}
dataType={item.type}
groupId={props.groupId}
type={props.type}
color={props.customAccentColor}
groupOptions={groupOptions}
itemCount={items?.placeholders.length}
@@ -322,11 +335,16 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
}
}
function getDate(item: Notebook | Note, groupType?: GroupingKey): number {
function getDate(
item: Notebook | Note,
groupType?: GroupingKey,
id?: string,
type?: GroupingByIdKey
): number {
return (
getSortValue(
groupType
? db.settings.getGroupOptions(groupType)
? getGroupOptions(groupType, id, type)
: {
sortBy: "dateEdited",
sortDirection: "desc"

View File

@@ -45,12 +45,15 @@ import { DefaultAppStyles } from "../../../utils/styles";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { useAsync } from "react-async-hook";
import { eMenuItemUpdate } from "../../../utils/events";
import { useIsFeatureAvailable } from "@notesnook/common";
import FormInput, {
createFormRef,
validators
} from "../../ui/input/form-input";
async function fetchMonographData(noteId: string) {
const monographId = db.monographs.monograph(noteId);
@@ -76,26 +79,31 @@ const PublishNoteSheet = ({
const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics");
const [isLocked, setIsLocked] = useState(false);
const [publishing, setPublishing] = useState(false);
const customTitle = useRef<string>("");
const pwdInput = useRef<TextInput>(null);
const titleInput = useRef<TextInput>(null);
const passwordValue = useRef<string>(undefined);
const monographData = useAsync(async () => {
return fetchMonographData(note?.id);
}, []);
const monograph = monographData.result?.monograph;
customTitle.current = monograph?.title || note.title || "";
const publishUrl = monograph && `${hosts.MONOGRAPH_HOST}/${monograph?.id}`;
const isPublished = db.monographs.monograph(note?.id);
const formRef = useRef(
createFormRef({
title: monograph?.title || note.title || "",
password: ""
})
);
useEffect(() => {
(async () => {
if (monograph) {
setSelfDestruct(!!monograph?.selfDestruct);
if (monograph.password) {
passwordValue.current = await db.monographs.decryptPassword(
const password = await db.monographs.decryptPassword(
monograph?.password
);
formRef.current.setValue("password", password);
setIsLocked(!!monograph?.password);
}
}
@@ -104,20 +112,23 @@ const PublishNoteSheet = ({
const publishNote = async () => {
if (publishing) return;
formRef.current.clearErrors();
if (!formRef.current.validate()) return;
const values = formRef.current.getValues();
setPublishLoading(true);
try {
if (note?.id) {
if (isLocked && !passwordValue.current) return;
await db.monographs.publish(note.id, customTitle.current, {
selfDestruct: selfDestruct,
password: isLocked ? passwordValue.current : undefined
await db.monographs.publish(note.id, values.title, {
selfDestruct,
password: isLocked ? values.password : undefined
});
await monographData.execute();
Navigation.queueRoutesForUpdate();
eSendEvent(eMenuItemUpdate);
setPublishLoading(false);
}
requestInAppReview();
} catch (e) {
@@ -127,9 +138,9 @@ const PublishNoteSheet = ({
type: "error",
context: "local"
});
} finally {
setPublishLoading(false);
}
setPublishLoading(false);
};
const setPublishLoading = (value: boolean) => {
setPublishing(value);
@@ -247,11 +258,12 @@ const PublishNoteSheet = ({
</TouchableOpacity>
) : null}
<Input
<FormInput
name="title"
formRef={formRef}
fwdRef={titleInput}
onChangeText={(value) => (customTitle.current = value)}
defaultValue={customTitle.current}
placeholder={strings.noteTitle()}
validators={[validators.required(strings.titleIsRequired())]}
/>
<TouchableOpacity
@@ -298,13 +310,16 @@ const PublishNoteSheet = ({
{isLocked ? (
<>
<Input
<FormInput
name="password"
formRef={formRef}
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue.current = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue.current}
placeholder={strings.enterPassword()}
validators={[
validators.required(strings.passwordRequired())
]}
containerStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}

View File

@@ -1,173 +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 { Item, ItemReference, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { RefObject, useEffect, useState } from "react";
import { View } from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../../common/database";
import {
PresentSheetOptions,
presentSheet
} from "../../../services/event-manager";
import { useRelationStore } from "../../../stores/use-relation-store";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import DialogHeader from "../../dialog/dialog-header";
import List from "../../list";
import SheetProvider from "../../sheet-provider";
import { Button, ButtonProps } from "../../ui/button";
import Paragraph from "../../ui/typography/paragraph";
type RelationsListProps = {
actionSheetRef: RefObject<ActionSheetRef>;
close?: () => void;
update?: (options: PresentSheetOptions) => void;
item: { id: string; type: string };
referenceType: string;
relationType: "to" | "from";
title: string;
button?: ButtonProps;
onAdd: () => void;
};
const IconsByType = {
reminder: "bell"
};
export const RelationsList = ({
actionSheetRef,
item,
referenceType,
relationType,
title,
button,
onAdd
}: RelationsListProps) => {
const updater = useRelationStore((state) => state.updater);
const { colors } = useThemeColors();
const [items, setItems] = useState<VirtualizedGrouping<Item>>();
const hasNoRelations = !items || items?.placeholders?.length === 0;
useEffect(() => {
db.relations?.[relationType]?.(
{ id: item?.id, type: item?.type } as ItemReference,
referenceType as any
)
.selector.sorted({
sortBy: "dateEdited",
sortDirection: "desc"
})
.then((grouped) => {
setTimeout(() => {
setItems(grouped);
}, 300);
});
}, [relationType, referenceType, item?.id, item?.type, updater]);
return (
<View style={{ paddingHorizontal: DefaultAppStyles.GAP, height: "100%" }}>
<SheetProvider context="local" />
<DialogHeader
title={title}
button={hasNoRelations ? undefined : button}
/>
{hasNoRelations ? (
<View
style={{
height: "85%",
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name={IconsByType[referenceType as keyof typeof IconsByType]}
size={60}
color={colors.primary.icon}
/>
<Paragraph>{strings.noLinksFound()}</Paragraph>
<Button
onPress={() => {
onAdd?.();
}}
fontSize={AppFontSize.sm}
// width="100%"
type="inverted"
icon="plus"
title={strings.addItem(
referenceType as "notebook" | "tag" | "reminder" | "note"
)}
/>
</View>
) : (
<List
data={items}
loading={false}
groupType={
referenceType === "note"
? "notes"
: referenceType === "tag"
? "tags"
: referenceType === "notebook"
? "notebooks"
: referenceType === "reminder"
? "reminders"
: "notes"
}
dataType={referenceType as any}
isRenderedInActionSheet={true}
/>
)}
</View>
);
};
RelationsList.present = ({
reference,
referenceType,
relationType,
title,
button,
onAdd
}: {
reference: { id: string; type: string };
referenceType: string;
relationType: "to" | "from";
title: string;
button?: ButtonProps;
onAdd: () => void;
}) => {
presentSheet({
component: (ref, close, update) => (
<RelationsList
actionSheetRef={ref}
close={close}
update={update}
item={reference}
referenceType={referenceType}
relationType={relationType}
title={title}
button={button}
onAdd={onAdd}
/>
)
});
};

View File

@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {
GroupingByIdKey,
GroupingKey,
GroupOptions,
ItemType,
@@ -27,7 +28,10 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import { View } from "react-native";
import { db } from "../../../common/database";
import {
getGroupOptions,
setGroupOptionsById
} from "../../../hooks/use-group-options";
import { eSendEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { RouteName } from "../../../stores/use-navigation-store";
@@ -43,20 +47,24 @@ import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const Sort = ({
type,
dataType,
screen,
hideGroupOptions,
group: groupType
group: groupType,
groupId,
type
}: {
type: ItemType;
dataType: ItemType;
type?: GroupingByIdKey;
screen?: RouteName;
group: GroupingKey;
hideGroupOptions?: boolean;
groupId?: string;
}) => {
const { colors } = useThemeColors();
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(groupType)
getGroupOptions(groupType, groupId, type)
);
const getSortButtonTitle = () => {
@@ -79,16 +87,17 @@ const Sort = ({
};
const updateGroupOptions = async (_groupOptions: GroupOptions) => {
await db.settings.setGroupOptions(groupType, _groupOptions);
console.log(groupId, type);
setGroupOptionsById(groupType, _groupOptions, groupId, type);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen) Navigation.queueRoutesForUpdate(screen);
if (type === "notebook") {
if (dataType === "notebook") {
useNotebookStore.getState().refresh();
} else if (type === "tag") {
} else if (dataType === "tag") {
useTagStore.getState().refresh();
}
eSendEvent(eGroupOptionsUpdated, groupType);
eSendEvent(eGroupOptionsUpdated, groupType, groupId, type);
eSendEvent(refreshNotesPage);
}, 1);
};

View File

@@ -493,7 +493,7 @@ const TabBar = (props: SimpleTabBarProps) => {
presentSheet({
component: (
<Sort
type={
dataType={
props.navigationState.index === 1
? "notebook"
: "tag"

View File

@@ -45,11 +45,11 @@ import ExportNotesSheet from "../components/sheets/export-notes";
import PaywallSheet from "../components/sheets/paywall";
import PublishNoteSheet from "../components/sheets/publish-note";
import { ReferencesList } from "../components/sheets/references";
import { RelationsList } from "../components/sheets/relations-list/index";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import { ButtonProps } from "../components/ui/button";
import AddReminder from "../screens/add-reminder";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import RelationsList from "../screens/relations-list";
import {
eSendEvent,
eSubscribeEvent,
@@ -784,6 +784,12 @@ export const useActions = ({
const duplicateNote = async () => {
await db.notes.duplicate(item.id);
Navigation.queueRoutesForUpdate();
ToastManager.show({
heading: strings.noteDuplicated(),
type: "success",
context: "local"
});
await sleep(500);
close();
};
@@ -1047,8 +1053,9 @@ export const useActions = ({
title: strings.dataTypesPluralCamelCase.reminder(),
icon: "clock-outline",
onPress: async () => {
close();
RelationsList.present({
reference: item,
item,
referenceType: "reminder",
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
@@ -1065,26 +1072,6 @@ export const useActions = ({
}
AddReminder.present(undefined, item);
close();
},
button: {
type: "plain",
onPress: async () => {
if (features && !features.activeReminders.isAllowed) {
ToastManager.show({
type: "info",
message: features.activeReminders.error,
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.activeReminders);
}
});
return;
}
AddReminder.present(undefined, item);
close();
},
icon: "plus",
iconSize: 20
}
});
},
@@ -1128,8 +1115,9 @@ export const useActions = ({
id: "add-reminder",
title: strings.remindMe(),
icon: "clock-plus-outline",
onPress: () => {
onPress: async () => {
close();
await sleep(100);
AddReminder.present(undefined, item);
}
},

View File

@@ -16,28 +16,61 @@ 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 { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { db } from "../common/database";
import { eSubscribeEvent, eUnSubscribeEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import { eGroupOptionsUpdated } from "../utils/events";
import { useSettingStore } from "../stores/use-setting-store";
import { GroupingByIdKey, GroupingKey, GroupOptions } from "@notesnook/core";
export function useGroupOptions(type: any) {
export function getGroupOptions(
groupingKey: GroupingKey,
id?: string,
type?: GroupingByIdKey
) {
return id && type
? db.settings?.getGroupOptionsById(id, type)
: db.settings?.getGroupOptions(groupingKey);
}
export function setGroupOptionsById(
groupingKey: GroupingKey,
groupOptions: GroupOptions,
id?: string,
type?: GroupingByIdKey
) {
return id && type
? db.settings?.setGroupOptionsById(id, type, groupOptions)
: db.settings?.setGroupOptions(groupingKey, groupOptions);
}
export function useGroupOptions(
groupingKey: GroupingKey,
id?: string,
type?: GroupingByIdKey
) {
const appLoading = useSettingStore((state) => state.isAppLoading);
const [groupOptions, setGroupOptions] = useState(
db.settings?.getGroupOptions(type)
getGroupOptions(groupingKey, id, type)
);
console.log(groupingKey, id, type, groupOptions, "options");
const groupOptionsRef = useRef(groupOptions);
groupOptionsRef.current = groupOptions;
useEffect(() => {
const onUpdate = (groupType: string) => {
if (groupType !== type) return;
const options = db.settings?.getGroupOptions(type) as any;
const onUpdate = (_groupingKey: string, _id?: string, _type?: string) => {
if (_groupingKey !== groupingKey) return;
if (_id && _type && _id !== id && _type !== type) return;
const options = getGroupOptions(groupingKey, id, type);
if (!options) return;
if (
groupOptions?.groupBy !== options.groupBy ||
groupOptions?.sortBy !== options.sortBy ||
groupOptions?.sortDirection !== groupOptions?.sortDirection
groupOptionsRef.current?.groupBy !== options.groupBy ||
groupOptionsRef.current?.sortBy !== options.sortBy ||
groupOptionsRef.current?.sortDirection !== options?.sortDirection
) {
console.log("onUpdate", _id, _type);
setGroupOptions({ ...options });
Navigation.queueRoutesForUpdate();
}
@@ -46,13 +79,13 @@ export function useGroupOptions(type: any) {
eSubscribeEvent(eGroupOptionsUpdated, onUpdate);
if (!appLoading) {
onUpdate(type);
onUpdate(groupingKey);
}
return () => {
eUnSubscribeEvent(eGroupOptionsUpdated, onUpdate);
};
}, [type, groupOptions, appLoading]);
}, [groupingKey, appLoading, id, type]);
return groupOptions;
}

View File

@@ -279,6 +279,7 @@ let MoveNotes: any = null;
let Settings: any = null;
let ManageTags: any = null;
let AddReminder: any = null;
let RelationsList: any = null;
let PayWall: any = null;
let Wrapped: any = null;
export const RootNavigation = () => {
@@ -385,6 +386,15 @@ export const RootNavigation = () => {
return AddReminder;
}}
/>
<RootStack.Screen
name="RelationsList"
getComponent={() => {
RelationsList =
RelationsList || require("../screens/relations-list").default;
return RelationsList;
}}
/>
<RootStack.Screen
name="PayWall"
getComponent={() => {

View File

@@ -95,7 +95,17 @@ const ReminderNotificationModes = {
export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params;
useNavigationFocus(props.navigation, { focusOnInit: true });
useNavigationFocus(props.navigation, {
focusOnInit: true,
onFocus: () => {
if (!props.route.params?.reminder) {
setTimeout(() => {
titleRef.current?.focus();
}, 200);
}
return false;
}
});
const { colors, isDark } = useThemeColors();
const weekFormat = useSettingStore((state) => state.weekFormat);
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
@@ -282,7 +292,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
defaultValue={reminder?.title || referencedItem?.title}
placeholder={strings.remindeMeOf()}
onChangeText={(text) => (title.current = text)}
autoFocus
wrapperStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}

View File

@@ -46,7 +46,6 @@ import EditorTabs from "../../../components/sheets/editor-tabs";
import { Issue } from "../../../components/sheets/github/issue";
import LinkNote from "../../../components/sheets/link-note";
import PaywallSheet from "../../../components/sheets/paywall";
import { RelationsList } from "../../../components/sheets/relations-list";
import TableOfContents from "../../../components/sheets/toc";
import { DDS } from "../../../services/device-detection";
import {
@@ -80,6 +79,7 @@ import { fluidTabsRef } from "../../../utils/global-refs";
import { sleep } from "../../../utils/time";
import AddReminder from "../../add-reminder";
import ManageTags from "../../manage-tags";
import RelationsList from "../../relations-list";
import { useDragState } from "../../settings/editor/state";
import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
@@ -466,7 +466,7 @@ export const useEditorEvents = (
const note = await db.notes.note(noteId);
if (!note) return;
RelationsList.present({
reference: note as any,
item: note as any,
referenceType: "reminder",
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),

View File

@@ -41,6 +41,7 @@ import { View } from "react-native";
import { Notebooks } from "../../components/sheets/notebooks";
import { useSettingStore } from "../../stores/use-setting-store";
import { rootNavigatorRef } from "../../utils/global-refs";
import { getGroupOptions } from "../../hooks/use-group-options";
const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
@@ -120,7 +121,9 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
params.current.id = notebook.id;
const notes = await db.relations
.from(notebook, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
.selector.grouped(
getGroupOptions("notes", notebook.id, "notebook")
);
setNotes(notes);
await notes.item(0, resolveItems);
syncWithNavigation();
@@ -190,6 +193,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
onRequestUpdate();
}}
id={params.current?.id}
type="notebook"
renderedInRoute="Notebook"
headerTitle={notebook?.title}
loading={loading}

View File

@@ -26,6 +26,7 @@ import useNavigationStore, {
NotesScreenParams
} from "../../stores/use-navigation-store";
import { PLACEHOLDER_DATA, openEditor } from "./common";
import { getGroupOptions } from "../../hooks/use-group-options";
export const ColoredNotes = ({
navigation,
route
@@ -52,7 +53,7 @@ ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
return await db.relations
.from({ id: params.id, type: "color" }, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
.selector.grouped(getGroupOptions("notes", params.id, "color"));
};
ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {

View File

@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { resolveItems } from "@notesnook/common";
import { Tag, VirtualizedGrouping } from "@notesnook/core";
import { Color, Note } from "@notesnook/core";
import { Color, Note, Tag, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import React, { useEffect, useRef, useState } from "react";
import { db } from "../../common/database";
import { FloatingButton } from "../../components/container/floating-button";
@@ -39,10 +39,9 @@ import useNavigationStore, {
NotesScreenParams,
RouteName
} from "../../stores/use-navigation-store";
import { setOnFirstSave } from "./common";
import { strings } from "@notesnook/intl";
import { useSettingStore } from "../../stores/use-setting-store";
import { rootNavigatorRef } from "../../utils/global-refs";
import { setOnFirstSave } from "./common";
export interface RouteProps<T extends RouteName> extends NavigationProps<T> {
get: (
@@ -172,7 +171,7 @@ const NotesPage = ({
if (loadingNotes) {
onRequestUpdate(params.current);
}
}, [loadingNotes, get, isAppLoading]);
}, [loadingNotes, get, isAppLoading, onRequestUpdate]);
useEffect(() => {
eSubscribeEvent(route.name, onRequestUpdate);
@@ -222,6 +221,7 @@ const NotesPage = ({
loading={false}
renderedInRoute={route.name}
id={params.current?.id}
type={params.current?.item?.type}
headerTitle={title || "Monographs"}
customAccentColor={accentColor}
placeholder={placeholder}

View File

@@ -26,6 +26,7 @@ import useNavigationStore, {
NotesScreenParams
} from "../../stores/use-navigation-store";
import { PLACEHOLDER_DATA, openEditor } from "./common";
import { getGroupOptions } from "../../hooks/use-group-options";
export const TaggedNotes = ({
navigation,
@@ -53,7 +54,7 @@ TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
return await db.relations
.from({ id: params.id, type: "tag" }, "note")
.selector.grouped(db.settings.getGroupOptions("notes"));
.selector.grouped(getGroupOptions("notes", params.id, "tag"));
};
TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {

View File

@@ -0,0 +1,142 @@
/*
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, ItemReference, VirtualizedGrouping } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { Header } from "../../components/header";
import List from "../../components/list";
import { Button } from "../../components/ui/button";
import Paragraph from "../../components/ui/typography/paragraph";
import Navigation, { NavigationProps } from "../../services/navigation";
import { useRelationStore } from "../../stores/use-relation-store";
import { AppFontSize } from "../../utils/size";
type RelationsListProps = {
item: Item;
referenceType: "notebook" | "tag" | "reminder" | "note";
relationType: "to" | "from";
title: string;
onAdd?: () => void;
};
const IconsByType = {
reminder: "bell"
};
function RelationsList(props: NavigationProps<"RelationsList">) {
const { item, referenceType, relationType, title, onAdd } = props.route
.params as RelationsListProps;
const updater = useRelationStore((state) => state.updater);
const { colors } = useThemeColors();
const [items, setItems] = useState<VirtualizedGrouping<Item>>();
const hasNoRelations = !items || items?.placeholders?.length === 0;
useEffect(() => {
db.relations?.[relationType]?.(
{ id: item?.id, type: item?.type } as ItemReference,
referenceType
)
.selector.sorted({
sortBy: "dateEdited",
sortDirection: "desc"
})
.then((grouped) => {
setTimeout(() => {
setItems(grouped);
}, 300);
});
}, [relationType, referenceType, item?.id, item?.type, updater]);
return (
<SafeAreaView
style={{
flex: 1,
backgroundColor: colors.primary.background,
width: "100%"
}}
>
<Header title={title} canGoBack />
<View style={{ flex: 1 }}>
{hasNoRelations ? (
<View
style={{
height: "85%",
justifyContent: "center",
alignItems: "center"
}}
>
<Icon
name={IconsByType[referenceType as keyof typeof IconsByType]}
size={60}
color={colors.primary.icon}
/>
<Paragraph>{strings.noLinksFound()}</Paragraph>
<Button
onPress={onAdd}
fontSize={AppFontSize.sm}
type="inverted"
icon="plus"
title={strings.addItem(referenceType)}
/>
</View>
) : (
<List
data={items}
loading={false}
groupType={
referenceType === "note"
? "notes"
: referenceType === "tag"
? "tags"
: referenceType === "notebook"
? "notebooks"
: referenceType === "reminder"
? "reminders"
: "notes"
}
dataType={referenceType}
/>
)}
</View>
</SafeAreaView>
);
}
RelationsList.present = ({
item,
relationType,
referenceType,
title,
onAdd
}: RelationsListProps) => {
Navigation.navigate("RelationsList", {
item,
relationType,
referenceType,
title,
onAdd
});
};
export default RelationsList;

View File

@@ -429,36 +429,61 @@ export const settingsGroups: SettingSection[] = [
{
id: "subscription-not-active",
name: strings.subscriptionNotActivated(),
hidden: () => Platform.OS !== "ios",
useHook: () => useUserStore((state) => state.user),
hidden: (user) =>
Platform.OS !== "ios" ||
(user as User)?.subscription?.plan !== SubscriptionPlan.FREE,
modifer: async () => {
if (Platform.OS === "android") return;
presentSheet({
title: strings.loadingSubscription(),
paragraph: strings.loadingSubscriptionDesc()
});
const subscriptions = await RNIap.getPurchaseHistory();
subscriptions.sort(
(a, b) => b.transactionDate - a.transactionDate
);
const currentSubscription = subscriptions[0];
presentSheet({
title: strings.notesnookPro(),
paragraph: strings.subscribedOnVerify(
new Date(currentSubscription.transactionDate).toLocaleString()
),
action: async () => {
presentSheet({
title: strings.verifySubscription(),
paragraph: strings.subscriptionVerifyWait()
try {
presentSheet({
title: strings.loadingSubscription(),
paragraph: strings.loadingSubscriptionDesc(),
progress: true
});
const subscriptions = await RNIap.getPurchaseHistory();
subscriptions.sort(
(a, b) => b.transactionDate - a.transactionDate
);
const currentSubscription = subscriptions[0];
if (
!currentSubscription ||
dayjs(currentSubscription.transactionDate).isBefore(
dayjs().subtract(30, "day")
)
) {
ToastManager.show({
message: "No active subscription found",
type: "info"
});
await PremiumService.subscriptions.verify(
currentSubscription
);
eSendEvent(eCloseSheet);
},
icon: "information-outline",
actionText: strings.verify()
});
return;
}
presentSheet({
title: strings.notesnookPro(),
paragraph: strings.subscribedOnVerify(
new Date(
currentSubscription.transactionDate
).toLocaleString()
),
action: async () => {
presentSheet({
title: strings.verifySubscription(),
paragraph: strings.subscriptionVerifyWait()
});
await PremiumService.subscriptions.verify(
currentSubscription
);
eSendEvent(eCloseSheet);
},
icon: "information-outline",
actionText: strings.verify()
});
} catch (e) {
eSendEvent(eCloseSheet);
}
},
description: strings.verifySubDesc()
},

View File

@@ -31,7 +31,11 @@ import type {
ThemesRouter
} from "@notesnook/themes-server";
import { keepLocalCopy, pick } from "@react-native-documents/picker";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
notifyManager,
QueryClient,
QueryClientProvider
} from "@tanstack/react-query";
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import { createTRPCReact } from "@trpc/react-query";
import React, { useState } from "react";
@@ -69,7 +73,8 @@ export const themeTrpcClient = createTRPCProxyClient<ThemesRouter>({
})
]
});
notifyManager.setBatchNotifyFunction((cb) => cb());
notifyManager.setNotifyFunction((cb) => cb());
function ThemeSelector() {
const [darkTheme, lightTheme] = useThemeStore((state) => [
state.darkTheme,
@@ -80,30 +85,20 @@ function ThemeSelector() {
const themeColors = colors;
const [searchQuery, setSearchQuery] = useState<string>();
const [colorScheme, setColorScheme] = useState<string>();
const filters = [];
if (searchQuery) filters.push({ type: "term" as const, value: searchQuery });
if (colorScheme)
filters.push({ type: "colorScheme" as const, value: colorScheme });
const themes = trpc.themes.useInfiniteQuery(
{
limit: 10,
compatibilityVersion: THEME_COMPATIBILITY_VERSION,
filters: [
...(searchQuery && searchQuery !== ""
? [
{
type: "term" as const,
value: searchQuery
}
]
: []),
...(colorScheme && colorScheme !== ""
? [
{
type: "colorScheme" as const,
value: colorScheme
}
]
: [])
]
filters
},
{
keepPreviousData: true,
getNextPageParam: (lastPage) => lastPage.nextCursor
}
);
@@ -366,12 +361,14 @@ function ThemeSelector() {
>
<View
style={{
flexDirection: "row"
flexDirection: "row",
gap: DefaultAppStyles.GAP_SMALL
}}
>
<Button
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
type={
colorScheme === "" || !colorScheme ? "accent" : "secondary"
@@ -384,7 +381,8 @@ function ThemeSelector() {
/>
<Button
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
type={colorScheme === "dark" ? "accent" : "secondary"}
title={strings.dark()}
@@ -395,7 +393,8 @@ function ThemeSelector() {
/>
<Button
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
fontSize={AppFontSize.xs}
type={colorScheme === "light" ? "accent" : "secondary"}
@@ -409,7 +408,8 @@ function ThemeSelector() {
<Button
title={strings.loadFromFile()}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
type={"secondaryAccented"}
icon="folder"

View File

@@ -71,6 +71,7 @@ const routeNames = {
Archive: "Archive",
ManageTags: "ManageTags",
AddReminder: "AddReminder",
RelationsList: "RelationsList",
PayWall: "PayWall",
Wrapped: "Wrapped"
};

View File

@@ -108,6 +108,13 @@ export interface RouteParams extends ParamListBase {
reminder?: Reminder;
reference?: Note;
};
RelationsList: {
item: Item;
referenceType: "notebook" | "tag" | "reminder" | "note";
relationType: "to" | "from";
title: string;
onAdd?: () => void;
};
Intro: GenericRouteParam;
PayWall: {
canGoBack?: boolean;

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.3.23",
"version": "3.3.24",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.23",
"version": "3.3.24",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -51,10 +51,10 @@
"@sayem314/react-native-keep-awake": "^1.3.1",
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.18.0",
"@trpc/client": "10.45.2",
"@trpc/react-query": "10.45.2",
"@trpc/server": "10.45.2",
"@types/validator": "^13.12.2",
"absolutify": "^0.1.0",
"async-mutex": "0.5.0",
@@ -5634,9 +5634,9 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.41.0.tgz",
"integrity": "sha512-193R4Jp9hjvlij6LryxrB5Mpbffd2L9PeWh3KlIy/hJV4SkBOfiQZ+jc5qAZLDCrdbkA5FjGj+UoDYw6TcNnyA==",
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.18.0.tgz",
"integrity": "sha512-PP4mG8MD08sq64RZCqMfXMYfaj7+Oulwg7xZ/fJoEOdTZNcPIgaOkHajZvUBsNLbi/0ViMvJB4cFkL2Jg2WPbw==",
"license": "MIT",
"funding": {
"type": "github",
@@ -5644,12 +5644,12 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.42.0.tgz",
"integrity": "sha512-j0tiofkzE3CSrYKmVRaKuwGgvCE+P2OOEDlhmfjeZf5ufcuFHwYwwgw3j08n4WYPVZ+OpsHblcFYezhKA3jDwg==",
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.18.0.tgz",
"integrity": "sha512-s1kdbGMdVcfUIllzsHUqVUdktBT5uuIRgnvrqFNLjl9TSOXEoBSDrhjsGjao0INQZv8cMpQlgOh3YH9YtN6cKw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "4.41.0",
"@tanstack/query-core": "4.18.0",
"use-sync-external-store": "^1.2.0"
},
"funding": {
@@ -5657,8 +5657,8 @@
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-native": "*"
},
"peerDependenciesMeta": {

View File

@@ -69,10 +69,10 @@
"@sayem314/react-native-keep-awake": "^1.3.1",
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.1-alpha.2",
"@tanstack/react-query": "^4.36.1",
"@trpc/client": "^10.45.2",
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.18.0",
"@trpc/client": "10.45.2",
"@trpc/react-query": "10.45.2",
"@trpc/server": "10.45.2",
"@types/validator": "^13.12.2",
"absolutify": "^0.1.0",
"async-mutex": "0.5.0",

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/web",
"version": "3.3.21",
"version": "3.3.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.3.21",
"version": "3.3.22",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.3.21",
"version": "3.3.22",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -46,7 +46,21 @@ export function DayPicker(props: DayPickerProps) {
maxDate,
minDate
},
years: { numberOfYears: 99 },
years: {
/**
* When minDate and maxDate are provided, we switch to "fluid" mode and calculate the number of years in a way that all valid years are visible.
*
* When not provided, use default mode and numberOfYears.
*/
numberOfYears:
minDate && maxDate
? Math.max(
Math.abs(maxDate.getFullYear() - selected.getFullYear()) + 1,
Math.abs(selected.getFullYear() - minDate.getFullYear()) + 1
) * 2
: 99,
mode: minDate && maxDate ? "fluid" : "decade"
},
calendar: {
startDay: 0
}
@@ -86,7 +100,10 @@ export function DayPicker(props: DayPickerProps) {
<Button
variant="icon"
sx={{ p: 0 }}
{...subtractOffset({ months: 1 }, { disabled: isPrevMonthBeforeMin })}
{...subtractOffset(
{ months: 1 },
{ disabled: isPrevMonthBeforeMin }
)}
>
<ChevronLeft />
</Button>

View File

@@ -27,7 +27,6 @@ import {
Lock,
NewTab,
Note,
NoteAdd,
NoteRemove,
Pin,
Plus,
@@ -98,7 +97,6 @@ type ToolButton = {
export function EditorActionBar() {
const { isMaximized, isFullscreen, hasNativeWindowControls } =
useWindowControls();
const isFocusMode = useAppStore((store) => store.isFocusMode);
const activeTab = useEditorStore((store) => store.getActiveTab());
const activeSession = useEditorStore((store) =>
activeTab ? store.getSession(activeTab.sessionId) : undefined
@@ -187,8 +185,7 @@ export function EditorActionBar() {
activeSession &&
activeSession.type !== "new" &&
activeSession.type !== "locked" &&
activeSession.type !== "conflicted" &&
!isFocusMode,
activeSession.type !== "conflicted",
onClick: () => useEditorStore.getState().toggleProperties(),
toggled: arePropertiesVisible
},

View File

@@ -45,6 +45,7 @@ import {
} from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useStore as useSearchStore } from "../../stores/search-store";
import type { Context } from "../list-container/types";
const groupByToTitleMap = {
none: "None",
@@ -61,6 +62,7 @@ type GroupingMenuOptions = {
groupingKey: GroupingKey;
refresh: () => void;
isSearching?: boolean;
context?: Context;
};
const groupByMenu: (options: GroupingMenuOptions) => MenuItem | null = (
@@ -192,6 +194,39 @@ export function showSortMenu(groupingKey: GroupingKey, refresh: () => void) {
);
}
function getGroupOptions(
context: Context | undefined,
isSearching: boolean | undefined,
groupingKey: GroupingKey
): GroupOptions {
return isSearching
? db.settings.getGroupOptions("search")
: context?.type === "notebook" ||
context?.type === "tag" ||
context?.type === "color"
? db.settings.getGroupOptionsById(context.id, context.type)
: db.settings.getGroupOptions(groupingKey);
}
async function setGroupOptions(
options: GroupingMenuOptions,
groupOptions: GroupOptions
) {
if (
options.context?.type === "notebook" ||
options.context?.type === "tag" ||
options.context?.type === "color"
) {
await db.settings.setGroupOptionsById(
options.context.id,
options.context.type,
groupOptions
);
} else {
await db.settings.setGroupOptions(options.groupingKey, groupOptions);
}
}
async function changeGroupOptions(
options: GroupingMenuOptions,
item: Omit<MenuButtonItem, "type">
@@ -207,7 +242,9 @@ async function changeGroupOptions(
? "dateModified"
: groupOptions.sortBy;
}
await db.settings.setGroupOptions(options.groupingKey, groupOptions);
await setGroupOptions(options, groupOptions);
if (options.groupingKey === "search")
useSearchStore.setState({ sortOptions: groupOptions });
options.refresh();
@@ -235,6 +272,7 @@ type GroupHeaderProps = {
onSelectGroup: () => void;
isFocused: boolean;
isSearching?: boolean;
context?: Context;
};
function GroupHeader(props: GroupHeaderProps) {
const {
@@ -246,10 +284,12 @@ function GroupHeader(props: GroupHeaderProps) {
refresh,
onSelectGroup,
isFocused,
isSearching
isSearching,
context
} = props;
const [groupOptions, setGroupOptions] = useState(
db.settings.getGroupOptions(isSearching ? "search" : groupingKey)
getGroupOptions(context, isSearching, groupingKey)
);
const groupHeaderRef = useRef<HTMLDivElement>(null);
const { openMenu, target } = useMenuTrigger();
@@ -359,8 +399,10 @@ function GroupHeader(props: GroupHeaderProps) {
groupByToTitleMap[groupOptions.groupBy || "default"]
}`}
onClick={() => {
const groupOptions = db.settings.getGroupOptions(
isSearching ? "search" : groupingKey
const groupOptions = getGroupOptions(
context,
isSearching,
groupingKey
);
setGroupOptions(groupOptions);
@@ -368,7 +410,8 @@ function GroupHeader(props: GroupHeaderProps) {
groupingKey: isSearching ? "search" : groupingKey,
groupOptions,
refresh,
isSearching
isSearching,
context
};
const groupBy = groupByMenu({
...menuOptions,

View File

@@ -370,6 +370,7 @@ function ItemRenderer({
title={resolvedItem.group.title}
isFocused={index === focusedGroupIndex}
index={index}
context={itemContext}
onSelectGroup={async () => {
if (!items.groups) return;

View File

@@ -41,7 +41,6 @@ import {
DefaultEditorSession
} from "../../stores/editor-store";
import { db } from "../../common/db";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useAttachmentStore } from "../../stores/attachment-store";
import { store as noteStore } from "../../stores/note-store";
import Toggle from "./toggle";
@@ -109,7 +108,6 @@ type EditorPropertiesProps = {
};
function EditorProperties(props: EditorPropertiesProps) {
const toggleProperties = useEditorStore((store) => store.toggleProperties);
const isFocusMode = useAppStore((store) => store.isFocusMode);
const dateFormat = useSettingStore((store) => store.dateFormat);
const timeFormat = useSettingStore((store) => store.timeFormat);
const metadataItems = [
@@ -140,7 +138,8 @@ function EditorProperties(props: EditorPropertiesProps) {
"diff"
])
);
if (isFocusMode || !session) return null;
if (!session) return null;
return (
<Flex
sx={{

View File

@@ -48,6 +48,8 @@ import Skeleton from "react-loading-skeleton";
dayjs.extend(customParseFormat);
const MAX_DATE = dayjs().add(99, "year").endOf("year").toDate();
export type AddReminderDialogProps = BaseDialogProps<boolean> & {
reminder?: Reminder;
note?: Note;
@@ -200,6 +202,14 @@ export const AddReminderDialog = DialogManager.register(
return;
}
if (mode !== Modes.REPEAT && date.isAfter(MAX_DATE)) {
showToast(
"error",
strings.maximumReminderDate(getFormattedDate(MAX_DATE, "date"))
);
return;
}
const id = await db.reminders.add({
id: reminder?.id,
recurringMode,
@@ -431,7 +441,7 @@ export const AddReminderDialog = DialogManager.register(
}}
selected={dayjs(date).toDate()}
minDate={new Date()}
maxDate={new Date(new Date().getFullYear() + 99, 11, 31)}
maxDate={MAX_DATE}
onSelect={(day) => {
if (!day) return;
const date = getFormattedDate(day, "date");

View File

@@ -65,6 +65,7 @@ export const EditNoteCreationDateDialog = DialogManager.register(
onClose(false);
}}
title={strings.editCreationDate()}
description={`${strings.note()}: ${strings.creationDateCannotBeAfterLastEditedDate()}`}
negativeButton={{
text: strings.cancel(),
onClick: () => {
@@ -76,14 +77,10 @@ export const EditNoteCreationDateDialog = DialogManager.register(
text: strings.save(),
onClick: async () => {
try {
if (date.isAfter(dayjs())) {
showToast("error", "Creation date cannot be in the future");
return;
}
if (dateEdited && date.isAfter(dayjs(dateEdited))) {
if (dateEdited && date.isAfter(dayjs(dateEdited), "minute")) {
showToast(
"error",
"Creation date cannot be after last edited date"
strings.creationDateCannotBeAfterLastEditedDate()
);
return;
}
@@ -140,7 +137,7 @@ export const EditNoteCreationDateDialog = DialogManager.register(
width: 300
}}
selected={dayjs(date).toDate()}
maxDate={new Date()}
maxDate={new Date(dateEdited)}
onSelect={(day) => {
if (!day) return;
const date = getFormattedDate(day, "date");

View File

@@ -54,19 +54,26 @@ export const NoteExpiryDateDialog = DialogManager.register(
return (
<Dialog
isOpen={true}
title={"Set Custom Expiry Date"}
title={strings.setExpiry()}
onClose={() => onClose(false)}
width={400}
positiveButton={{
text: strings.done(),
onClick: async () => {
if (date.isBefore(dayjs())) {
showToast("error", "Expiry date must be in the future");
showToast("error", strings.expiryDateMustBeInTheFuture());
return;
}
if (date.isAfter(dayjs().add(1, "year"))) {
showToast(
"error",
strings.expiryDateCannotBeMoreThan1YearInTheFuture()
);
return;
}
await db.notes.setExpiryDate(date.valueOf(), noteId);
store.refresh();
showToast("success", "Expiry date set");
showToast("success", strings.expiryDateSet());
onClose(true);
}
}}

View File

@@ -714,7 +714,10 @@ export async function getUploadedFileSize(filename: string) {
headers: { Authorization: `Bearer ${token}` }
});
const contentLength = parseInt(attachmentInfo.headers["content-length"]);
const contentLength = parseInt(
attachmentInfo.headers["x-object-size"] ??
attachmentInfo.headers["content-length"]
);
return isNaN(contentLength) ? 0 : contentLength;
} catch (e) {
logger.error(e, "Failed to get uploaded file size.", { filename });

View File

@@ -57,18 +57,23 @@ class NoteStore extends BaseStore<NoteStore> {
};
setContext = async (context?: Context) => {
const groupOptions =
context?.type === "notebook" ||
context?.type === "tag" ||
context?.type === "color"
? db.settings.getGroupOptionsById(context.id, context.type)
: db.settings.getGroupOptions(
context?.type === "favorite"
? "favorites"
: context?.type === "archive"
? "archive"
: "notes"
);
this.set({
context,
contextNotes: context
? await notesFromContext(context).grouped(
db.settings.getGroupOptions(
context.type === "favorite"
? "favorites"
: context.type === "archive"
? "archive"
: "notes"
)
)
? await notesFromContext(context).grouped(groupOptions)
: undefined
});
};

View File

@@ -38,6 +38,7 @@ export function Notebooks() {
const roots = useStore((store) => store.notebooks);
const [filteredNotebooks, setFilteredNotebooks] =
useState<VirtualizedGrouping<NotebookType>>();
const inputRef = useRef<HTMLInputElement>(null);
const treeRef =
useRef<
VirtualizedTreeHandle<{ notebook: NotebookType; totalNotes: number }>
@@ -61,6 +62,13 @@ export function Notebooks() {
useEffect(() => {
treeRef.current?.refresh();
const query = inputRef.current?.value.trim();
if (!query) return;
(async () => {
setFilteredNotebooks(await db.lookup.notebooks(query).sorted());
})();
}, [roots]);
return (
@@ -165,6 +173,7 @@ export function Notebooks() {
)}
</Box>
<Input
ref={inputRef}
variant="clean"
placeholder="Filter notebooks..."
sx={{ borderTop: "1px solid var(--border)", mx: 0 }}

View File

@@ -23,23 +23,31 @@ import Placeholder from "../components/placeholders";
import { db } from "../common/db";
import { ListLoader } from "../components/loaders/list-loader";
import { Flex, Input } from "@theme-ui/components";
import { forwardRef, useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { debounce } from "@notesnook/common";
import { Tag, VirtualizedGrouping } from "@notesnook/core";
import ScrollContainer from "../components/scroll-container";
import { ScrollerProps } from "react-virtuoso";
import { SidebarScroller } from "../components/sidebar-scroller";
function Tags() {
const tags = useStore((store) => store.tags);
const refresh = useStore((store) => store.refresh);
const [filteredTags, setFilteredTags] = useState<VirtualizedGrouping<Tag>>();
const inputRef = useRef<HTMLInputElement>(null);
const items = filteredTags || tags;
useEffect(() => {
store.refresh();
}, []);
useEffect(() => {
const query = inputRef.current?.value.trim();
if (!query) return;
(async () => {
setFilteredTags(await db.lookup.tags(query).sorted());
})();
}, [tags]);
if (!items) return <ListLoader />;
return (
<Flex
@@ -62,6 +70,7 @@ function Tags() {
Scroller={SidebarScroller}
/>
<Input
ref={inputRef}
variant="clean"
placeholder="Filter tags..."
sx={{ borderTop: "1px solid var(--border)", mx: 0 }}

View File

@@ -47,3 +47,29 @@ test("save trash cleanup interval", () =>
await db.settings.setTrashCleanupInterval(interval);
expect(db.settings.getTrashCleanupInterval()).toBe(interval);
}));
const GROUP_OPTIONS_BY_ID_TESTS = ["notebook", "tag", "color"];
for (const type of GROUP_OPTIONS_BY_ID_TESTS) {
test(`get ${type} id group options`, () =>
databaseTest().then(async (db) => {
const id = `test-${type}-id`;
const groupOptions = {
groupBy: "year",
sortBy: "title",
sortDirection: "asc"
};
await db.settings.setGroupOptionsById(id, type, groupOptions);
expect(db.settings.getGroupOptionsById(id, type)).toMatchObject(
groupOptions
);
}));
test(`get ${type} id group options fallback to notes group options`, () =>
databaseTest().then(async (db) => {
const id = `non-existent-${type}-id`;
const defaultOptions = db.settings.getGroupOptions("notes");
const result = db.settings.getGroupOptionsById(id, type);
expect(result).toMatchObject(defaultOptions);
}));
}

View File

@@ -32,7 +32,8 @@ import {
TrashCleanupInterval,
TimeFormat,
DayFormat,
WeekFormat
WeekFormat,
GroupingByIdKey
} from "../types.js";
import { ICollection } from "./collection.js";
import { SQLCachedCollection } from "../database/sql-cached-collection.js";
@@ -64,6 +65,9 @@ const defaultSettings: SettingItemMap = {
trashCleanupInterval: 7,
profile: undefined,
"groupOptions:notes:notebooks": {},
"groupOptions:notes:tags": {},
"groupOptions:notes:colors": {},
"groupOptions:trash": DEFAULT_GROUP_OPTIONS("trash"),
"groupOptions:tags": DEFAULT_GROUP_OPTIONS("tags"),
"groupOptions:notes": DEFAULT_GROUP_OPTIONS("notes"),
@@ -150,6 +154,34 @@ export class Settings implements ICollection {
return this.set(`groupOptions:${key}`, groupOptions);
}
async setGroupOptionsById(
id: string,
type: GroupingByIdKey,
groupOptions: GroupOptions
) {
const groupOptionsKey =
type === "notebook"
? "groupOptions:notes:notebooks"
: type === "tag"
? "groupOptions:notes:tags"
: "groupOptions:notes:colors";
const groupOptionsMap = this.get(groupOptionsKey);
groupOptionsMap[id] = groupOptions;
return this.set(groupOptionsKey, groupOptionsMap);
}
getGroupOptionsById(id: string, type: GroupingByIdKey) {
const groupOptions = this.get(
type === "notebook"
? "groupOptions:notes:notebooks"
: type === "tag"
? "groupOptions:notes:tags"
: "groupOptions:notes:colors"
);
return groupOptions[id] || this.get("groupOptions:notes");
}
setToolbarConfig(platform: ToolbarConfigPlatforms, config: ToolbarConfig) {
return this.set(`toolbarConfig:${platform}`, config);
}

View File

@@ -57,6 +57,7 @@ export const GroupingKey = [
"search"
] as const;
export type GroupingKey = (typeof GroupingKey)[number];
export type GroupingByIdKey = "notebook" | "tag" | "color";
export type ValueOf<T> = T[keyof T];
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
@@ -486,6 +487,12 @@ export type SettingItemMap = {
defaultTag: string | undefined;
profile: Profile | undefined;
} & Record<`groupOptions:${GroupingKey}`, GroupOptions> &
Record<
| `groupOptions:notes:notebooks`
| `groupOptions:notes:tags`
| `groupOptions:notes:colors`,
Record<string, GroupOptions>
> &
Record<`toolbarConfig:${ToolbarConfigPlatforms}`, ToolbarConfig | undefined> &
Record<`sideBarOrder:${SideBarSection}`, string[]> &
Record<`sideBarHiddenItems:${SideBarHideableSection}`, string[]>;

View File

@@ -139,6 +139,7 @@ function Header({
}): JSX.Element {
const tab = useTabContext();
const editor = editors[tab.id];
const tableOfContents = editorControllers[tab.id]?.getTableOfContents?.();
const insets = useSafeArea();
const openedTabsCount = useTabStore((state) => state.tabs.length);
const [isOpen, setOpen] = useState(false);
@@ -148,6 +149,8 @@ function Header({
state.canGoForward
]);
console.log(tableOfContents?.length);
return (
<div
style={{
@@ -496,26 +499,29 @@ function Header({
</span>
</MenuItem>
<MenuItem
value="toc"
style={{
display: "flex",
gap: 10,
alignItems: "center"
}}
>
<TableOfContentsIcon
size={20 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
<span
{tableOfContents?.length ? (
<MenuItem
value="toc"
style={{
color: "var(--nn_primary_paragraph)"
display: "flex",
gap: 10,
alignItems: "center"
}}
>
{strings.toc()}
</span>
</MenuItem>
<TableOfContentsIcon
size={20 * settings.fontScale}
color="var(--nn_primary_icon)"
/>
<span
style={{
color: "var(--nn_primary_paragraph)"
}}
>
{strings.toc()}
</span>
</MenuItem>
) : null}
<MenuItem
value="scroll-top"
style={{

View File

@@ -113,13 +113,19 @@ function Tags(props: { settings: Settings; loading?: boolean }) {
backgroundColor:
index !== 0 ? "transparent" : "var(--nn_secondary_background)",
borderRadius: 6,
padding: "0px 4px",
height: "24px",
padding: "2px 4px",
height: "25px",
fontFamily: "Inter",
fontSize: 12,
color: "var(--nn_primary_icon)",
userSelect: "none",
WebkitUserSelect: "none"
WebkitUserSelect: "none",
textAlign: "left",
maxWidth: 150,
textOverflow: "ellipsis",
overflow: "hidden",
wordWrap: "break-word",
whiteSpace: "nowrap"
}}
onClick={(e) => {
e.preventDefault();

View File

@@ -17,17 +17,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Flex, Text } from "@theme-ui/components";
import { useCallback, useState } from "react";
import { Popup } from "../components/popup.js";
import { Input, Textarea } from "@theme-ui/components";
import { Embed, EmbedSizeOptions } from "../../extensions/embed/index.js";
import { convertUrlToEmbedUrl } from "@social-embed/lib";
import { InlineInput } from "../../components/inline-input/index.js";
import { Tabs, Tab } from "../../components/tabs/index.js";
import { strings } from "@notesnook/intl";
import { convertUrlToEmbedUrl, isValidUrl } from "@social-embed/lib";
import { Flex, Input, Text, Textarea } from "@theme-ui/components";
import { useCallback, useState } from "react";
import { InlineInput } from "../../components/inline-input/index.js";
import { Tab, Tabs } from "../../components/tabs/index.js";
import { Embed, EmbedSizeOptions } from "../../extensions/embed/index.js";
import { Popup } from "../components/popup.js";
type EmbedSource = "url" | "code";
export type EmbedPopupProps = {
onClose: (embed?: Embed) => void;
title: string;
@@ -38,33 +38,30 @@ export type EmbedPopupProps = {
export function EmbedPopup(props: EmbedPopupProps) {
const { onClose, onSizeChanged, title, embed } = props;
const [width, setWidth] = useState(embed?.width || 300);
const [height, setHeight] = useState(embed?.height || 150);
const [src, setSrc] = useState(embed?.src || "");
const [embedSource, setEmbedSource] = useState<EmbedSource>("url");
const [error, setError] = useState<string | null>(null);
const [size, setSize] = useState<EmbedSizeOptions>({
width: embed?.width || 300,
height: embed?.height || 150
});
const onSizeChange = useCallback(
(newWidth?: number, newHeight?: number) => {
const size: EmbedSizeOptions = newWidth
? {
width: newWidth,
height: newWidth * (height / width)
}
: newHeight
? {
width: newHeight * (width / height),
height: newHeight
}
: {
width: 0,
height: 0
};
setWidth(size.width);
setHeight(size.height);
if (onSizeChanged) onSizeChanged(size);
const hasNewWidth = Number.isFinite(newWidth);
const hasNewHeight = Number.isFinite(newHeight);
if (!hasNewWidth && !hasNewHeight) return;
setSize((size) => {
const newSize = {
width: hasNewWidth ? ((newWidth || 0) as number) : size.width,
height: hasNewHeight ? ((newHeight || 0) as number) : size.height
};
if (onSizeChanged) onSizeChanged(newSize);
return newSize;
});
},
[height, width, onSizeChanged]
[onSizeChanged]
);
return (
@@ -77,8 +74,9 @@ export function EmbedPopup(props: EmbedPopupProps) {
onClick: () => {
setError(null);
let _src = src;
let _width = width;
let _height = height;
let _width = size.width;
let _height = size.height;
if (embedSource === "code") {
const document = new DOMParser().parseFromString(src, "text/html");
if (document.getElementsByTagName("iframe").length <= 0)
@@ -100,8 +98,19 @@ export function EmbedPopup(props: EmbedPopupProps) {
if (heightValue && !isNaN(parseInt(heightValue)))
_height = parseInt(heightValue);
}
if (embedSource === "url" && !isValidUrl(src)) {
return setError("Please provide a valid url.");
}
const convertedUrl = convertUrlToEmbedUrl(_src);
if (convertedUrl) _src = convertedUrl;
if (!_src && embedSource === "url") {
return setError("Please provide a valid embed url.");
}
if (_src.startsWith("javascript:")) {
return setError("Embedding javascript code is not supported.");
}
@@ -147,7 +156,7 @@ export function EmbedPopup(props: EmbedPopupProps) {
label="width"
type="number"
placeholder={strings.width()}
value={width}
defaultValue={size.width}
sx={{
mr: 1,
fontSize: "body"
@@ -158,7 +167,7 @@ export function EmbedPopup(props: EmbedPopupProps) {
label="height"
type="number"
placeholder={strings.height()}
value={height}
defaultValue={size.height}
sx={{ fontSize: "body" }}
onChange={(e) =>
onSizeChange(undefined, e.target.valueAsNumber)

View File

@@ -1973,6 +1973,10 @@ msgstr "Created at"
msgid "Creating a{0} backup"
msgstr "Creating a{0} backup"
#: src/strings.ts:2695
msgid "Creation date cannot be after last edited date"
msgstr "Creation date cannot be after last edited date"
#: src/strings.ts:2049
msgid "Credentials"
msgstr "Credentials"
@@ -2767,6 +2771,18 @@ msgstr "Experience the next level of private note taking\""
msgid "Expiry date"
msgstr "Expiry date"
#: src/strings.ts:2692
msgid "Expiry date cannot be more than 1 year in the future"
msgstr "Expiry date cannot be more than 1 year in the future"
#: src/strings.ts:2690
msgid "Expiry date must be in the future"
msgstr "Expiry date must be in the future"
#: src/strings.ts:2693
msgid "Expiry date set"
msgstr "Expiry date set"
#: src/strings.ts:2550
msgid "Explore all plans"
msgstr "Explore all plans"
@@ -3930,6 +3946,10 @@ msgstr "Math & formulas"
msgid "Maximize"
msgstr "Maximize"
#: src/strings.ts:2698
msgid "Maximum reminder date is {maxDate}"
msgstr "Maximum reminder date is {maxDate}"
#: src/strings.ts:2080
msgid "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions."
msgstr "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions."
@@ -4293,6 +4313,10 @@ msgstr "Note copied to clipboard"
msgid "Note does not exist"
msgstr "Note does not exist"
#: src/strings.ts:2696
msgid "Note duplicated"
msgstr "Note duplicated"
#: src/strings.ts:458
msgid "Note history"
msgstr "Note history"

View File

@@ -1962,6 +1962,10 @@ msgstr ""
msgid "Creating a{0} backup"
msgstr ""
#: src/strings.ts:2695
msgid "Creation date cannot be after last edited date"
msgstr ""
#: src/strings.ts:2049
msgid "Credentials"
msgstr ""
@@ -2756,6 +2760,18 @@ msgstr ""
msgid "Expiry date"
msgstr ""
#: src/strings.ts:2692
msgid "Expiry date cannot be more than 1 year in the future"
msgstr ""
#: src/strings.ts:2690
msgid "Expiry date must be in the future"
msgstr ""
#: src/strings.ts:2693
msgid "Expiry date set"
msgstr ""
#: src/strings.ts:2550
msgid "Explore all plans"
msgstr ""
@@ -3910,6 +3926,10 @@ msgstr ""
msgid "Maximize"
msgstr ""
#: src/strings.ts:2698
msgid "Maximum reminder date is {maxDate}"
msgstr ""
#: src/strings.ts:2080
msgid "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions."
msgstr ""
@@ -4273,6 +4293,10 @@ msgstr ""
msgid "Note does not exist"
msgstr ""
#: src/strings.ts:2696
msgid "Note duplicated"
msgstr ""
#: src/strings.ts:458
msgid "Note history"
msgstr ""

View File

@@ -2686,5 +2686,14 @@ Continue without attachments?`,
openingLocalFileDesc: (filePath: string) =>
t`Are you sure you want to open this file: ${filePath}?`,
cantOpenFileLinksInBrowsers: () =>
t`File links cannot be opened in browsers. Please use the Notesnook desktop app.`
t`File links cannot be opened in browsers. Please use the Notesnook desktop app.`,
expiryDateMustBeInTheFuture: () => t`Expiry date must be in the future`,
expiryDateCannotBeMoreThan1YearInTheFuture: () =>
t`Expiry date cannot be more than 1 year in the future`,
expiryDateSet: () => t`Expiry date set`,
creationDateCannotBeAfterLastEditedDate: () =>
t`Creation date cannot be after last edited date`,
noteDuplicated: () => t`Note duplicated`,
maximumReminderDate: (maxDate: string) =>
t`Maximum reminder date is ${maxDate}`
};