Compare commits

...

58 Commits

Author SHA1 Message Date
Ammar Ahmed
dfb1dc7e57 mobile: do not request any permissions on android 2026-06-08 13:11:00 +05:00
Ammar Ahmed
c27e8916a1 mobile: fix toast message not shown 2026-06-08 13:11:00 +05:00
Ammar Ahmed
5fe6331c1f mobile: fix saving qr-code to gallery 2026-06-08 13:11:00 +05:00
Ammar Ahmed
68d1a1bc34 Merge pull request #9894 from kashaf-ansari-dev/fix-theme-invalid-file-upload-error
mobile: show user-friendly error for invalid theme file upload
2026-06-08 13:09:32 +05:00
kashaf-ansari-dev
7adc5fbf1e mobile: copy actual error message for incomplete theme file uploads
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-08 13:09:10 +05:00
kashaf-ansari-dev
46b6888ca3 mobile: add error message for incomplete theme files
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-08 13:09:10 +05:00
Ammar Ahmed
9eec90e333 mobile: fix invalid error shown on iOS if no file is picked 2026-06-08 13:09:10 +05:00
kashaf-ansari-dev
e49291c55e mobile: add Learn more action to theme import invalid JSON file error and modified error message
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-08 13:09:10 +05:00
kashaf-ansari-dev
70147d4332 mobile: show user-friendly error for invalid theme file upload
Signed-off-by: kashaf-ansari-dev <kashafansari3108@gmail.com>
2026-06-08 13:08:53 +05:00
Ammar Ahmed
f8d2649f4d Merge pull request #9944 from streetwriters/fix/517
mobile: wrap title input into multiple lines for publish note
2026-06-08 13:07:25 +05:00
Ammar Ahmed
537c514562 Merge pull request #9953 from streetwriters/mobile/release-3.3.25
Mobile/release 3.3.25
2026-06-08 10:23:15 +05:00
copilot-swe-agent[bot]
d9720ba7b8 ci: make iOS artifact upload non-blocking 2026-06-06 10:11:35 +00:00
Ammar Ahmed
16e968dc57 mobile: release v3.3.25 2026-06-06 10:11:50 +05:00
Ammar Ahmed
03fafcbd15 mobile: fix attachment file-size check 2026-06-06 10:11:25 +05:00
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
59b003d1f8 mobile: limit input height to 100 2026-06-04 20:24:22 +05:00
Ammar Ahmed
50d21e8787 mobile: wrap title input into multiple lines for publish note 2026-06-04 20:21:10 +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
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
01zulfi
52d7d90b68 web: bump version to 3.3.21
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2026-06-01 10:21:57 +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
Abdullah Atta
aab6a36067 Do not refetch user before decrypting each chunk in sync (#9805)
* core: do not refetch user before decrypting each chunk in sync

* web: fix editor readonly state not updating in realtime
2026-05-25 19:42:03 +05:00
01zulfi
c7e0cd35e7 editor: expand collapsed nodes on search scroll if search text inside callout (#9745)
* editor: expand callout on search scroll if search text inside callout
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: expand outline list && headings on search scroll
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: expand parents & scroll into view on type

* editor: fix parents expanding on every transaction

* editor: add a small debounce to delay search

* editor: fix editor not scrolling to selected search result

---------

Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2026-05-25 19:41:06 +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
63 changed files with 1103 additions and 519 deletions

View File

@@ -116,6 +116,7 @@ jobs:
api-private-key: ${{ secrets.API_KEY }}
- name: Upload Notesnook.ipa to Github
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: Notesnook.zip

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/desktop",
"version": "3.3.20",
"version": "3.3.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/desktop",
"version": "3.3.20",
"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.20",
"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

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3105
versionCode 3106
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')

View File

@@ -115,6 +115,10 @@ export const FileSizeResult = {
Error: -1
};
function getFileSizeFromHeaders(headers: Headers) {
return headers.get("x-object-size") || headers.get("content-length");
}
export async function getUploadedFileSize(hash: string, retry = 0) {
try {
const url = `${hosts.API_HOST}/s3?name=${hash}`;
@@ -124,24 +128,21 @@ export async function getUploadedFileSize(hash: string, retry = 0) {
headers: { Authorization: `Bearer ${token}` }
});
if (
!attachmentInfo.ok ||
attachmentInfo.headers?.get("content-length") === null
) {
const fileSize = getFileSizeFromHeaders(attachmentInfo.headers);
if (!attachmentInfo.ok || fileSize === null) {
if (retry < 3) {
DatabaseLogger.log(`Retrying file size check: ${hash}, ${retry}`);
return getUploadedFileSize(hash, retry + 1);
}
throw new Error(
`File size check failed: ${hash}, ${
attachmentInfo.status
}, ${attachmentInfo.headers?.get("content-length")}`
`File size check failed: ${hash}, ${attachmentInfo.status}, ${fileSize}`
);
}
const contentLength = parseInt(
attachmentInfo.headers?.get("content-length") as string
);
console.log(attachmentInfo.headers);
const contentLength = parseInt(fileSize as string);
return isNaN(contentLength) ? FileSizeResult.Empty : contentLength;
} catch (e) {
DatabaseLogger.error(e);
@@ -161,10 +162,10 @@ export async function checkUpload(
size === 0
? `File size is 0.`
: size === -1
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
? `File verification check failed.`
: expectedSize !== decryptedLength
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
: undefined;
if (error) throw new Error(error);
}
@@ -191,7 +192,8 @@ export async function checkAndCreateDir(path: string) {
}
export const santizeUri = (uri: string) => {
return Platform.OS === "ios" ? decodeURI(uri).replace("file:///", "/") : uri;
const decoded = decodeURI(uri);
return Platform.OS === "ios" ? decoded.replace("file:///", "/") : decoded;
};
export function isSuccessStatusCode(statusCode: number) {

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,17 @@ const PublishNoteSheet = ({
</TouchableOpacity>
) : null}
<Input
<FormInput
name="title"
formRef={formRef}
fwdRef={titleInput}
onChangeText={(value) => (customTitle.current = value)}
defaultValue={customTitle.current}
multiline
scrollEnabled
containerStyle={{
maxHeight: 100
}}
placeholder={strings.noteTitle()}
validators={[validators.required(strings.titleIsRequired())]}
/>
<TouchableOpacity
@@ -298,13 +315,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

@@ -21,7 +21,7 @@ import { sanitizeFilename } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { createRef } from "react";
import { Platform, View } from "react-native";
import { PermissionsAndroid, Platform, View } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import FileViewer from "react-native-file-viewer";
import * as ScopedStorage from "react-native-scoped-storage";
@@ -45,6 +45,7 @@ import SheetWrapper from "../../ui/sheet";
import { QRCode } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
import { CameraRoll } from "@react-native-camera-roll/camera-roll";
class RecoveryKeySheet extends React.Component {
constructor(props) {
@@ -108,22 +109,14 @@ class RecoveryKeySheet extends React.Component {
saveQRCODE = async () => {
this.svg.current?.toDataURL(async (data) => {
try {
let path;
let fileName = "nn_" + this.user.email + "_recovery_key_qrcode";
let fileName =
"nn_" + this.user.email + "_recovery_key_qrcode" + "_" + Date.now();
fileName = sanitizeFilename(fileName, { replacement: "_" });
fileName = fileName + ".png";
if (Platform.OS === "android") {
await ScopedStorage.createDocument(
fileName,
"image/png",
data,
"base64"
);
} else {
path = await filesystem.checkAndCreateDir("/");
await RNFetchBlob.fs.writeFile(path + fileName, data, "base64");
}
const path = RNFetchBlob.fs.dirs.CacheDir + fileName;
await RNFetchBlob.fs.writeFile(path, data, "base64");
await CameraRoll.saveToCameraRoll(`file://` + path);
ToastManager.show({
heading: strings.recoveryKeyQRCodeSaved(),
type: "success",

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
}
});
},

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

@@ -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";
@@ -59,6 +63,8 @@ import { getElevationStyle } from "../../utils/elevation";
import { MenuItemsList } from "../../utils/menu-items";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { openLinkInBrowser } from "../../utils/functions";
import Clipboard from "@react-native-clipboard/clipboard";
const THEME_SERVER_URL = "https://themes-api.notesnook.com";
//@ts-ignore
@@ -69,7 +75,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 +87,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 +363,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 +383,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 +395,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 +410,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"
@@ -441,14 +443,55 @@ function ThemeSelector() {
ReactNativeBlobUtil.fs
.unlink(themeJsonCopiedPath)
.catch(() => {});
const json = JSON.parse(themeJson);
let json;
try {
json = JSON.parse(themeJson);
} catch (e) {
ToastManager.show({
heading: strings.invalidThemeFileFormat(),
type: "error",
context: "global",
actionText: strings.learnMore(),
func: () => {
openLinkInBrowser(
"https://help.notesnook.com/custom-themes/introduction"
);
}
});
return;
}
const result = validateTheme(json);
if (result.error) {
ToastManager.error(new Error(result.error));
if (
typeof result.error === "string" &&
result.error.includes("missing from the theme")
) {
ToastManager.show({
heading: strings.themeMissingRequiredFields(),
type: "error",
context: "global",
actionText: strings.copyLogs(),
func: () => {
Clipboard.setString(result.error || "");
ToastManager.show({
heading: strings.logsCopied(),
type: "success",
context: "global"
});
}
});
} else {
ToastManager.error(new Error(result.error));
}
return;
}
select(json, true);
} catch (e) {
if ((e as Error).message.includes("Code=3072")) {
return;
}
ToastManager.error(e as Error);
}
}}

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

@@ -630,6 +630,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -643,6 +644,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -781,6 +783,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -794,6 +797,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -839,6 +843,7 @@
"${PODS_CONFIGURATION_BUILD_DIR}/TOCropViewController/TOCropViewControllerBundle.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/react-native-cameraroll/RNCameraRollPrivacyInfo.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -852,6 +857,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TOCropViewControllerBundle.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCameraRollPrivacyInfo.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;

View File

@@ -1914,6 +1914,34 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-cameraroll (7.10.2):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-config (1.5.7):
- react-native-config/App (= 1.5.7)
- react-native-config/App (1.5.7):
@@ -3522,6 +3550,7 @@ DEPENDENCIES:
- react-native-background-actions (from `../node_modules/react-native-background-actions`)
- react-native-begin-background-task (from `../node_modules/react-native-begin-background-task`)
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-config (from `../node_modules/react-native-config`)
- react-native-date-picker (from `../node_modules/react-native-date-picker`)
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
@@ -3717,6 +3746,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-begin-background-task"
react-native-blob-util:
:path: "../node_modules/react-native-blob-util"
react-native-cameraroll:
:path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-config:
:path: "../node_modules/react-native-config"
react-native-date-picker:
@@ -3941,6 +3972,7 @@ SPEC CHECKSUMS:
react-native-background-actions: 40e09df6ea8c7d2753f6c1d75f6f1eee4a9bc35f
react-native-begin-background-task: 2191f2a84b0328932a3d44db4361e666fee781bb
react-native-blob-util: 7946b7e13acf0da5e849dc2f73fcfebe1d981699
react-native-cameraroll: bb98380ee21115d5fe1ae0f8b80c86e044613746
react-native-config: 963b5efabc864cf69412e54b5de49b6a23e4af03
react-native-date-picker: 4f4f40f6e65798038bb4b1bff47890c2be69c2e6
react-native-document-picker: d624d3d9bd9311da87f6f7b64aa44f69927d8543

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2184
IOS_MARKETING_VERSION = 3.3.24
IOS_CURRENT_PROJECT_VERSION = 2185
IOS_MARKETING_VERSION = 3.3.25
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2184
IOS_MARKETING_VERSION = 3.3.24
IOS_CURRENT_PROJECT_VERSION = 2185
IOS_MARKETING_VERSION = 3.3.25
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Staging iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2184
IOS_MARKETING_VERSION = 3.3.24
IOS_CURRENT_PROJECT_VERSION = 2185
IOS_MARKETING_VERSION = 3.3.25
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.3.23",
"version": "3.3.25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.23",
"version": "3.3.25",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -37,6 +37,7 @@
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
"@react-native-camera-roll/camera-roll": "^7.10.2",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/checkbox": "^0.5.20",
"@react-native-community/datetimepicker": "^8.4.5",
@@ -51,10 +52,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",
@@ -4197,6 +4198,18 @@
"node": ">=14"
}
},
"node_modules/@react-native-camera-roll/camera-roll": {
"version": "7.10.2",
"resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-7.10.2.tgz",
"integrity": "sha512-XgJQJDFUycmqSX+MH7vTcRigQwEIQNLIu1GvOngCZRwlSV2mF61UzeruSmmHwkBcGnHZFXkKg9fil0FQVfyglw==",
"license": "MIT",
"engines": {
"node": ">= 18.17.0"
},
"peerDependencies": {
"react-native": ">=0.59"
}
},
"node_modules/@react-native-clipboard/clipboard": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz",
@@ -5634,9 +5647,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 +5657,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 +5670,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

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.24",
"version": "3.3.25",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
@@ -55,6 +55,7 @@
"@notesnook/logger": "file:../../packages/logger",
"@notesnook/theme": "file:../../packages/theme",
"@notesnook/themes-server": "file:../../servers/themes",
"@react-native-camera-roll/camera-roll": "^7.10.2",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/checkbox": "^0.5.20",
"@react-native-community/datetimepicker": "^8.4.5",
@@ -69,10 +70,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.20",
"version": "3.3.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/web",
"version": "3.3.20",
"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.20",
"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

@@ -96,19 +96,7 @@ const features: Record<FeatureKeys, Feature> = {
)
}
]
: [
{
icon: File,
title: "Improved attachments UX",
subtitle:
"We've improved the UI/UX of attaching multiple files into the editor. The entire process is now handled in a unified dialog."
},
{
icon: InternalLink,
title: "Opening file links on desktop",
subtitle: "The NN Desktop app can now open file links (file:///)."
}
],
: [],
cta: {
title: strings.gotIt(),
icon: Checkmark,

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

@@ -286,7 +286,8 @@ class EditorStore extends BaseStore<EditorStore> {
const clearIds: string[] = [];
for (const session of sessions) {
if (session.type === "new") continue;
if (session.note.id !== item.id && session.note.contentId !== item.id) continue;
if (session.note.id !== item.id && session.note.contentId !== item.id)
continue;
if (isDeleted(item) || isTrashItem(item))
clearIds.push(session.tabId);
// if a note becomes conflicted, reopen the session
@@ -331,6 +332,13 @@ class EditorStore extends BaseStore<EditorStore> {
!item.readonly
)
openSession(session.note.id, { force: true, silent: true });
// if a note is made readonly, reopen the session
else if (
session.type !== "readonly" &&
item.type === "note" &&
item.readonly
)
openSession(session.note.id, { force: true, silent: true });
// update the note in all sessions
else if (item.type === "note") {
updateSession(

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

@@ -0,0 +1,3 @@
- Bug fixes and improvements
Thank you for using Notesnook!

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

@@ -404,7 +404,9 @@ class UserManager {
const masterKey = await this.getMasterKey();
if (!masterKey) return;
const dataEncryptionKey = await this.keyManager.get("dataEncryptionKey");
const dataEncryptionKey = await this.keyManager.get("dataEncryptionKey", {
refetchUser: false
});
if (!dataEncryptionKey)
return [
{
@@ -415,7 +417,10 @@ class UserManager {
const keys: { version: KeyVersion; key: SerializedKey }[] = [];
const legacyDataEncryptionKey = await this.keyManager.get(
"legacyDataEncryptionKey"
"legacyDataEncryptionKey",
{
refetchUser: false
}
);
if (legacyDataEncryptionKey)
keys.push({

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

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Extension } from "@tiptap/core";
import { Decoration, DecorationSet } from "prosemirror-view";
import { Decoration, DecorationSet, EditorView } from "prosemirror-view";
import {
EditorState,
Plugin,
@@ -28,6 +28,7 @@ import {
} from "prosemirror-state";
import { SearchSettings } from "../../toolbar/stores/search-store.js";
import { tiptapKeys } from "@notesnook/common";
import { toggleNodesUnderPos } from "../heading/index.js";
type DispatchFn = (tr: Transaction) => void;
declare module "@tiptap/core" {
@@ -295,12 +296,11 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
)
);
const domNode = this.editor.view.domAtPos(from).node;
scrollIntoView(domNode);
this.storage.selectedIndex = nextIndex;
tr.setMeta("isSearching", true);
tr.setMeta("selectedIndex", nextIndex);
if (dispatch) updateView(state, dispatch);
return true;
},
moveToPreviousResult:
@@ -322,10 +322,8 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
)
);
const domNode = this.editor.view.domAtPos(from).node;
scrollIntoView(domNode);
this.storage.selectedIndex = prevIndex;
tr.setMeta("isSearching", true);
tr.setMeta("selectedIndex", prevIndex);
if (dispatch) updateView(state, dispatch);
@@ -470,14 +468,90 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
decorations(state) {
return key.getState(state).results;
}
},
appendTransaction: (transactions, oldState, newState) => {
const isSearchTransaction = transactions.find((t) =>
t.getMeta("isSearching")
);
const selectedResult =
this.storage.results?.[this.storage.selectedIndex];
if (!isSearchTransaction || !selectedResult) return;
const tr = newState.tr;
scrollIntoView(this.editor.view, selectedResult.from);
if (expandCollapsedParents(tr, selectedResult.from)) {
return tr;
}
}
})
];
}
});
function scrollIntoView(domNode: Node) {
function expandCollapsedParents(tr: Transaction, pos: number) {
try {
let changed = false;
const $pos = tr.doc.resolve(pos);
for (let depth = 1; depth <= $pos.depth; depth++) {
const node = $pos.node(depth);
const nodePos = $pos.before(depth);
if (
(node.type.name === "callout" ||
node.type.name === "outlineListItem") &&
node.attrs.collapsed
) {
tr.setNodeAttribute(nodePos, "collapsed", false);
changed = true;
}
// expand collapsed heading that hid this node via hidden attribute
if (node.attrs.hidden) {
const parentNode = $pos.node(depth - 1);
const parentContentStart = depth === 1 ? 0 : $pos.before(depth - 1) + 1;
let collapsedHeadingPos = -1;
let collapsedHeadingLevel = -1;
parentNode.forEach((child, offset) => {
const childAbsPos = parentContentStart + offset;
if (childAbsPos >= nodePos) return;
if (
child.type.name === "heading" &&
child.attrs.collapsed &&
!child.attrs.hidden
) {
collapsedHeadingPos = childAbsPos;
collapsedHeadingLevel = child.attrs.level;
}
});
if (collapsedHeadingPos !== -1) {
tr.setNodeAttribute(collapsedHeadingPos, "collapsed", false);
toggleNodesUnderPos(
tr,
collapsedHeadingPos,
collapsedHeadingLevel,
false
);
changed = true;
}
}
}
if (changed) tr.setMeta("preventSave", true);
return changed;
} catch (e) {
console.error("Error expanding collapsed parents: ", e);
}
}
function scrollIntoView(view: EditorView, pos: number) {
setTimeout(() => {
const domNode = view.domAtPos(pos).node;
if ("scrollIntoView" in domNode) {
(domNode as Element).scrollIntoView({
behavior: "instant",

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

@@ -25,6 +25,7 @@ import { ToolButton } from "../components/tool-button.js";
import { Editor } from "../../types.js";
import { useEditorSearchStore } from "../stores/search-store.js";
import { strings } from "@notesnook/intl";
import { inlineDebounce } from "@notesnook/common";
export type SearchReplacePopupProps = { editor: Editor };
export function SearchReplacePopup(props: SearchReplacePopupProps) {
@@ -94,7 +95,7 @@ export function SearchReplacePopup(props: SearchReplacePopupProps) {
sx={{ p: 0, fontFamily: "monospace" }}
value={searchTerm}
onChange={(e) => {
search(e.target.value);
inlineDebounce("search", () => search(e.target.value), 100);
useEditorSearchStore.setState({ searchTerm: e.target.value });
}}
onKeyDown={(e) => {

View File

@@ -1868,6 +1868,10 @@ msgstr "Copy link"
msgid "Copy link text"
msgstr "Copy link text"
#: src/strings.ts:2703
msgid "Copy logs"
msgstr "Copy logs"
#: src/strings.ts:454
msgid "Copy note"
msgstr "Copy note"
@@ -1973,6 +1977,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 +2775,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 +3950,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 +4317,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"
@@ -4650,6 +4678,10 @@ msgstr "Payment method"
msgid "PDF is password protected"
msgstr "PDF is password protected"
#: src/strings.ts:2705
msgid "Permission required to save QR-Code to Gallery"
msgstr "Permission required to save QR-Code to Gallery"
#: src/strings.ts:1729
msgid "phone number"
msgstr "phone number"
@@ -7249,6 +7281,14 @@ msgstr "We are creating a backup of your data. Please wait..."
msgid "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
msgstr "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
#: src/strings.ts:2700
msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
msgstr "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
#: src/strings.ts:2702
msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
msgstr "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
#: src/strings.ts:1390
msgid "We have sent you an email confirmation link. Please check your email inbox. If you cannot find the email, check your spam folder."
msgstr "We have sent you an email confirmation link. Please check your email inbox. If you cannot find the email, check your spam folder."

View File

@@ -1857,6 +1857,10 @@ msgstr ""
msgid "Copy link text"
msgstr ""
#: src/strings.ts:2703
msgid "Copy logs"
msgstr ""
#: src/strings.ts:454
msgid "Copy note"
msgstr ""
@@ -1962,6 +1966,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 +2764,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 +3930,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 +4297,10 @@ msgstr ""
msgid "Note does not exist"
msgstr ""
#: src/strings.ts:2696
msgid "Note duplicated"
msgstr ""
#: src/strings.ts:458
msgid "Note history"
msgstr ""
@@ -4624,6 +4652,10 @@ msgstr ""
msgid "PDF is password protected"
msgstr ""
#: src/strings.ts:2705
msgid "Permission required to save QR-Code to Gallery"
msgstr ""
#: src/strings.ts:1729
msgid "phone number"
msgstr ""
@@ -7197,6 +7229,14 @@ msgstr ""
#: src/strings.ts:474
msgid "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
msgstr "<<<<<<< HEAD"
#: src/strings.ts:2700
msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file."
msgstr ""
#: src/strings.ts:2702
msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties."
msgstr ""
#: src/strings.ts:1390

View File

@@ -2686,5 +2686,21 @@ 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}`,
invalidThemeFileFormat: () =>
t`We couldn't load this theme. Please make sure the file is a valid JSON theme file.`,
themeMissingRequiredFields: () =>
t`We couldn't load this theme. The file appears to be incomplete or missing required theme properties.`,
copyLogs: () => t`Copy logs`,
permissionRequiredToSaveQRCode: () =>
t`Permission required to save QR-Code to Gallery`
};