mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
58 Commits
fix/357
...
mobile/311
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb1dc7e57 | ||
|
|
c27e8916a1 | ||
|
|
5fe6331c1f | ||
|
|
68d1a1bc34 | ||
|
|
7adc5fbf1e | ||
|
|
46b6888ca3 | ||
|
|
9eec90e333 | ||
|
|
e49291c55e | ||
|
|
70147d4332 | ||
|
|
f8d2649f4d | ||
|
|
537c514562 | ||
|
|
d9720ba7b8 | ||
|
|
16e968dc57 | ||
|
|
03fafcbd15 | ||
|
|
b7bd06c9f3 | ||
|
|
766a8f28d4 | ||
|
|
86512b5ce7 | ||
|
|
59b003d1f8 | ||
|
|
50d21e8787 | ||
|
|
a820fcde53 | ||
|
|
5ddf1eddf5 | ||
|
|
4237d2eb4a | ||
|
|
f67cd503d6 | ||
|
|
14039d6a1e | ||
|
|
d1fed9072e | ||
|
|
b003a3ffed | ||
|
|
55e10e4755 | ||
|
|
7c1dd0f79a | ||
|
|
229cf195a8 | ||
|
|
38da6ac08b | ||
|
|
597dc587cd | ||
|
|
5c76b92789 | ||
|
|
8418d5fa9c | ||
|
|
cd071f8f58 | ||
|
|
32ddc3fa40 | ||
|
|
68d07a7862 | ||
|
|
2aa5162fc9 | ||
|
|
cf5565d09d | ||
|
|
f129e0c411 | ||
|
|
bb43d80e05 | ||
|
|
4e4d3866df | ||
|
|
b2088ef782 | ||
|
|
82d37701c5 | ||
|
|
b48ab3c85d | ||
|
|
52d7d90b68 | ||
|
|
bb7ff4f376 | ||
|
|
43b35917ef | ||
|
|
bcfa34ca05 | ||
|
|
aab6a36067 | ||
|
|
c7e0cd35e7 | ||
|
|
bd3edc535a | ||
|
|
1dff7a121d | ||
|
|
3f3467e3a7 | ||
|
|
39bb9afa0e | ||
|
|
886e757715 | ||
|
|
5c93645969 | ||
|
|
07defdfa78 | ||
|
|
27b00d4ef9 |
1
.github/workflows/ios.publish.yml
vendored
1
.github/workflows/ios.publish.yml
vendored
@@ -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
|
||||
|
||||
4
apps/desktop/package-lock.json
generated
4
apps/desktop/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -493,7 +493,7 @@ const TabBar = (props: SimpleTabBarProps) => {
|
||||
presentSheet({
|
||||
component: (
|
||||
<Sort
|
||||
type={
|
||||
dataType={
|
||||
props.navigationState.index === 1
|
||||
? "notebook"
|
||||
: "tag"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
142
apps/mobile/app/screens/relations-list/index.tsx
Normal file
142
apps/mobile/app/screens/relations-list/index.tsx
Normal 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;
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -71,6 +71,7 @@ const routeNames = {
|
||||
Archive: "Archive",
|
||||
ManageTags: "ManageTags",
|
||||
AddReminder: "AddReminder",
|
||||
RelationsList: "RelationsList",
|
||||
PayWall: "PayWall",
|
||||
Wrapped: "Wrapped"
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
43
apps/mobile/package-lock.json
generated
43
apps/mobile/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
4
apps/web/package-lock.json
generated
4
apps/web/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -370,6 +370,7 @@ function ItemRenderer({
|
||||
title={resolvedItem.group.title}
|
||||
isFocused={index === focusedGroupIndex}
|
||||
index={index}
|
||||
context={itemContext}
|
||||
onSelectGroup={async () => {
|
||||
if (!items.groups) return;
|
||||
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
3
fastlane/metadata/android/en-US/changelogs/15534.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/15534.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
- Bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[]>;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user