diff --git a/.github/workflows/ios.publish.yml b/.github/workflows/ios.publish.yml index a8c046e85..631003e2f 100644 --- a/.github/workflows/ios.publish.yml +++ b/.github/workflows/ios.publish.yml @@ -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 diff --git a/apps/desktop/src/api/spell-checker.ts b/apps/desktop/src/api/spell-checker.ts index 54e79c291..2559effe9 100644 --- a/apps/desktop/src/api/spell-checker.ts +++ b/apps/desktop/src/api/spell-checker.ts @@ -84,35 +84,55 @@ const LANGUAGES: Record = { type Language = { code: string; name: string }; +const LANGUAGE_REDIRECT_MAP: Record = { + 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( - () => - ( - globalThis.window?.webContents.session.availableSpellCheckerLanguages.map( - (code) => ({ - code, - name: LANGUAGES[code] - }) - ) - ) - ), - enabledLanguages: t.procedure.query( - () => - ( - 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 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 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; +} diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 6fd784813..8dd698fb1 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -140,7 +140,7 @@ android { if (project.hasProperty("prBuildNumber")) { versionCode Integer.parseInt(prBuildNumber()) } else { - versionCode 3105 + versionCode 3107 } versionName getNpmVersion() testBuildType System.getProperty('testBuildType', 'debug') diff --git a/apps/mobile/app/common/filesystem/utils.ts b/apps/mobile/app/common/filesystem/utils.ts index 425af83cb..e5f87a39e 100644 --- a/apps/mobile/app/common/filesystem/utils.ts +++ b/apps/mobile/app/common/filesystem/utils.ts @@ -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) { diff --git a/apps/mobile/app/components/list-items/headers/section-header.tsx b/apps/mobile/app/components/list-items/headers/section-header.tsx index 7f5d09e01..e52d6e4a9 100644 --- a/apps/mobile/app/components/list-items/headers/section-header.tsx +++ b/apps/mobile/app/components/list-items/headers/section-header.tsx @@ -19,6 +19,7 @@ along with this program. If not, see . 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: ( . */ -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 ] ); diff --git a/apps/mobile/app/components/list/list-item.wrapper.tsx b/apps/mobile/app/components/list/list-item.wrapper.tsx index 0bf7db65e..7ef5f3eca 100644 --- a/apps/mobile/app/components/list/list-item.wrapper.tsx +++ b/apps/mobile/app/components/list/list-item.wrapper.tsx @@ -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 = { group: GroupingKey; @@ -62,6 +63,8 @@ type ListItemWrapperProps = { 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" diff --git a/apps/mobile/app/components/sheets/publish-note/index.tsx b/apps/mobile/app/components/sheets/publish-note/index.tsx index 1b9b7722e..9544a3176 100644 --- a/apps/mobile/app/components/sheets/publish-note/index.tsx +++ b/apps/mobile/app/components/sheets/publish-note/index.tsx @@ -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); @@ -81,27 +84,32 @@ const PublishNoteSheet = ({ const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics"); const [isLocked, setIsLocked] = useState(false); const [publishing, setPublishing] = useState(false); - const customTitle = useRef(""); const pwdInput = useRef(null); const titleInput = useRef(null); - const passwordValue = useRef(undefined); const monographData = useAsync(async () => { return fetchMonographData(note?.id); }, []); const monograph = monographData.result?.monograph; const metadata = monographData.result?.metadata; - customTitle.current = monograph?.title || note.title || ""; const publishUrl = metadata?.publishUrl || monograph?.publishUrl || ""; 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); } } @@ -110,20 +118,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) { @@ -133,9 +144,9 @@ const PublishNoteSheet = ({ type: "error", context: "local" }); + } finally { + setPublishLoading(false); } - - setPublishLoading(false); }; const setPublishLoading = (value: boolean) => { setPublishing(value); @@ -253,11 +264,17 @@ const PublishNoteSheet = ({ ) : null} - (customTitle.current = value)} - defaultValue={customTitle.current} + multiline + scrollEnabled + containerStyle={{ + maxHeight: 100 + }} placeholder={strings.noteTitle()} + validators={[validators.required(strings.titleIsRequired())]} /> - (passwordValue.current = value)} blurOnSubmit secureTextEntry - defaultValue={passwordValue.current} placeholder={strings.enterPassword()} + validators={[ + validators.required(strings.passwordRequired()) + ]} containerStyle={{ marginTop: DefaultAppStyles.GAP_VERTICAL }} diff --git a/apps/mobile/app/components/sheets/recovery-key/index.jsx b/apps/mobile/app/components/sheets/recovery-key/index.jsx index 7b0825d0e..6e01e3c73 100644 --- a/apps/mobile/app/components/sheets/recovery-key/index.jsx +++ b/apps/mobile/app/components/sheets/recovery-key/index.jsx @@ -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", diff --git a/apps/mobile/app/components/sheets/relations-list/index.tsx b/apps/mobile/app/components/sheets/relations-list/index.tsx deleted file mode 100644 index 7bb9ab8ae..000000000 --- a/apps/mobile/app/components/sheets/relations-list/index.tsx +++ /dev/null @@ -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 . -*/ -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; - 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>(); - 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 ( - - - - {hasNoRelations ? ( - - - {strings.noLinksFound()} - diff --git a/apps/web/src/components/editor/action-bar.tsx b/apps/web/src/components/editor/action-bar.tsx index 1ee1e6efa..6eeb712d5 100644 --- a/apps/web/src/components/editor/action-bar.tsx +++ b/apps/web/src/components/editor/action-bar.tsx @@ -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 }, diff --git a/apps/web/src/components/group-header/index.tsx b/apps/web/src/components/group-header/index.tsx index 1050215a8..6d55aa32b 100644 --- a/apps/web/src/components/group-header/index.tsx +++ b/apps/web/src/components/group-header/index.tsx @@ -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 @@ -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(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, diff --git a/apps/web/src/components/list-container/index.tsx b/apps/web/src/components/list-container/index.tsx index db35aa79c..83af42828 100644 --- a/apps/web/src/components/list-container/index.tsx +++ b/apps/web/src/components/list-container/index.tsx @@ -370,6 +370,7 @@ function ItemRenderer({ title={resolvedItem.group.title} isFocused={index === focusedGroupIndex} index={index} + context={itemContext} onSelectGroup={async () => { if (!items.groups) return; diff --git a/apps/web/src/components/properties/index.tsx b/apps/web/src/components/properties/index.tsx index 677e36eba..32b8ed4e9 100644 --- a/apps/web/src/components/properties/index.tsx +++ b/apps/web/src/components/properties/index.tsx @@ -42,7 +42,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"; @@ -119,7 +118,6 @@ type EditorPropertiesProps = { function EditorProperties(props: EditorPropertiesProps) { const toggleProperties = useEditorStore((store) => store.toggleProperties); useSpellChecker((store) => store.enabled); - const isFocusMode = useAppStore((store) => store.isFocusMode); const dateFormat = useSettingStore((store) => store.dateFormat); const timeFormat = useSettingStore((store) => store.timeFormat); const metadataItems = [ @@ -150,7 +148,8 @@ function EditorProperties(props: EditorPropertiesProps) { "diff" ]) ); - if (isFocusMode || !session) return null; + if (!session) return null; + return ( & { 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"); diff --git a/apps/web/src/dialogs/edit-note-creation-date-dialog.tsx b/apps/web/src/dialogs/edit-note-creation-date-dialog.tsx index d73e76dd0..1a85ef215 100644 --- a/apps/web/src/dialogs/edit-note-creation-date-dialog.tsx +++ b/apps/web/src/dialogs/edit-note-creation-date-dialog.tsx @@ -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"); diff --git a/apps/web/src/dialogs/feature-dialog.tsx b/apps/web/src/dialogs/feature-dialog.tsx index 635e410c6..65363d6f1 100644 --- a/apps/web/src/dialogs/feature-dialog.tsx +++ b/apps/web/src/dialogs/feature-dialog.tsx @@ -96,19 +96,7 @@ const features: Record = { ) } ] - : [ - { - 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, diff --git a/apps/web/src/dialogs/note-expiry-date-dialog.tsx b/apps/web/src/dialogs/note-expiry-date-dialog.tsx index 42173b28b..01ae5604d 100644 --- a/apps/web/src/dialogs/note-expiry-date-dialog.tsx +++ b/apps/web/src/dialogs/note-expiry-date-dialog.tsx @@ -54,19 +54,26 @@ export const NoteExpiryDateDialog = DialogManager.register( return ( 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); } }} diff --git a/apps/web/src/interfaces/fs.ts b/apps/web/src/interfaces/fs.ts index fda669c2e..7049b0a1f 100644 --- a/apps/web/src/interfaces/fs.ts +++ b/apps/web/src/interfaces/fs.ts @@ -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 }); diff --git a/apps/web/src/stores/note-store.ts b/apps/web/src/stores/note-store.ts index 63d1caf22..6a3a7278e 100644 --- a/apps/web/src/stores/note-store.ts +++ b/apps/web/src/stores/note-store.ts @@ -58,18 +58,23 @@ class NoteStore extends BaseStore { }; 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 }); }; diff --git a/apps/web/src/views/notebooks.tsx b/apps/web/src/views/notebooks.tsx index 88a03e570..0e33fec43 100644 --- a/apps/web/src/views/notebooks.tsx +++ b/apps/web/src/views/notebooks.tsx @@ -38,6 +38,7 @@ export function Notebooks() { const roots = useStore((store) => store.notebooks); const [filteredNotebooks, setFilteredNotebooks] = useState>(); + const inputRef = useRef(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() { )} store.tags); const refresh = useStore((store) => store.refresh); const [filteredTags, setFilteredTags] = useState>(); + const inputRef = useRef(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 ; return ( 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); + })); +} diff --git a/packages/core/src/collections/settings.ts b/packages/core/src/collections/settings.ts index 58c97e652..8ed265fd5 100644 --- a/packages/core/src/collections/settings.ts +++ b/packages/core/src/collections/settings.ts @@ -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"; @@ -65,6 +66,9 @@ const defaultSettings: SettingItemMap = { profile: undefined, "vault:lockAfter": 1000 * 60 * 30, + "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"), @@ -151,6 +155,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); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4f988823f..4cf8f2903 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -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[keyof T]; export type Optional = Pick, K> & Omit; @@ -491,6 +492,12 @@ export type SettingItemMap = { profile: Profile | undefined; "vault:lockAfter": number; } & Record<`groupOptions:${GroupingKey}`, GroupOptions> & + Record< + | `groupOptions:notes:notebooks` + | `groupOptions:notes:tags` + | `groupOptions:notes:colors`, + Record + > & Record<`toolbarConfig:${ToolbarConfigPlatforms}`, ToolbarConfig | undefined> & Record<`sideBarOrder:${SideBarSection}`, string[]> & Record<`sideBarHiddenItems:${SideBarHideableSection}`, string[]>; diff --git a/packages/editor-mobile/src/components/header.tsx b/packages/editor-mobile/src/components/header.tsx index 718064a09..833a70449 100644 --- a/packages/editor-mobile/src/components/header.tsx +++ b/packages/editor-mobile/src/components/header.tsx @@ -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 (
- - - - {strings.toc()} - - + + + {strings.toc()} + + + ) : null} + { e.preventDefault(); diff --git a/packages/editor/src/toolbar/popups/embed-popup.tsx b/packages/editor/src/toolbar/popups/embed-popup.tsx index cb634952b..52747e407 100644 --- a/packages/editor/src/toolbar/popups/embed-popup.tsx +++ b/packages/editor/src/toolbar/popups/embed-popup.tsx @@ -17,17 +17,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -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("url"); const [error, setError] = useState(null); + const [size, setSize] = useState({ + 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) diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index c830f7b02..815c6184a 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -1,18 +1,11 @@ msgid "" msgstr "" -"POT-Creation-Date: 2026-05-21 08:29+0500\n" -"POT-Creation-Date: 2026-05-21 08:29+0500\n" +"POT-Creation-Date: 2026-06-10 17:36+0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: @lingui/cli\n" "Language: en\n" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: \n" -"Last-Translator: \n" -"Language-Team: \n" -"Plural-Forms: \n" #: src/strings.ts:2423 msgid " \"Notebook > Notes\"" @@ -1940,6 +1933,10 @@ msgstr "Copy link" msgid "Copy link text" msgstr "Copy link text" +#: src/strings.ts:2784 +msgid "Copy logs" +msgstr "Copy logs" + #: src/strings.ts:454 msgid "Copy note" msgstr "Copy note" @@ -2065,6 +2062,10 @@ msgstr "Creating a{0} backup" msgid "Creating..." msgstr "Creating..." +#: src/strings.ts:2776 +msgid "Creation date cannot be after last edited date" +msgstr "Creation date cannot be after last edited date" + #: src/strings.ts:2051 msgid "Credentials" msgstr "Credentials" @@ -2903,6 +2904,18 @@ msgstr "Expires on" msgid "Expiry date" msgstr "Expiry date" +#: src/strings.ts:2773 +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:2771 +msgid "Expiry date must be in the future" +msgstr "Expiry date must be in the future" + +#: src/strings.ts:2774 +msgid "Expiry date set" +msgstr "Expiry date set" + #: src/strings.ts:2552 msgid "Explore all plans" msgstr "Explore all plans" @@ -4131,6 +4144,10 @@ msgstr "Math & formulas" msgid "Maximize" msgstr "Maximize" +#: src/strings.ts:2779 +msgid "Maximum reminder date is {maxDate}" +msgstr "Maximum reminder date is {maxDate}" + #: src/strings.ts:2082 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." @@ -4506,6 +4523,10 @@ msgstr "Note copied to clipboard" msgid "Note does not exist" msgstr "Note does not exist" +#: src/strings.ts:2777 +msgid "Note duplicated" +msgstr "Note duplicated" + #: src/strings.ts:458 msgid "Note history" msgstr "Note history" @@ -4867,6 +4888,10 @@ msgstr "Payment method" msgid "PDF is password protected" msgstr "PDF is password protected" +#: src/strings.ts:2786 +msgid "Permission required to save QR-Code to Gallery" +msgstr "Permission required to save QR-Code to Gallery" + #: src/strings.ts:1731 msgid "phone number" msgstr "phone number" @@ -7522,6 +7547,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:2781 +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:2783 +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:1392 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." diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 7803782f2..e0aa824f1 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -1,18 +1,11 @@ msgid "" msgstr "" -"POT-Creation-Date: 2026-05-21 08:29+0500\n" -"POT-Creation-Date: 2026-05-21 08:29+0500\n" +"POT-Creation-Date: 2026-06-10 17:36+0500\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: @lingui/cli\n" "Language: pseudo-LOCALE\n" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: \n" -"Last-Translator: \n" -"Language-Team: \n" -"Plural-Forms: \n" #: src/strings.ts:2423 msgid " \"Notebook > Notes\"" @@ -978,11 +971,11 @@ msgstr "" #: src/strings.ts:1326 msgid "Are you sure you want to clear trash?" -msgstr "<<<<<<< HEAD=======" +msgstr "" #: src/strings.ts:2757 msgid "Are you sure you want to delete all failed inbox items?" -msgstr ">>>>>>> 101851125 (mobile: add failed inbox item history)" +msgstr "" #: src/strings.ts:2734 msgid "Are you sure you want to delete this attachment?" @@ -994,7 +987,7 @@ msgstr "" #: src/strings.ts:775 msgid "Are you sure you want to logout from this device? Any unsynced changes will be lost." -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2768 msgid "Are you sure you want to open this file: {filePath}?" @@ -1709,7 +1702,7 @@ msgstr "" #: src/strings.ts:1383 msgid "Close" -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2760 msgid "Close ({seconds})" @@ -1822,11 +1815,11 @@ msgstr "" #: src/strings.ts:144 msgid "Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases." -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2761 msgid "Compressing" -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2765 msgid "Compression failed" @@ -1929,6 +1922,10 @@ msgstr "" msgid "Copy link text" msgstr "" +#: src/strings.ts:2784 +msgid "Copy logs" +msgstr "" + #: src/strings.ts:454 msgid "Copy note" msgstr "" @@ -2054,6 +2051,10 @@ msgstr "" msgid "Creating..." msgstr "" +#: src/strings.ts:2776 +msgid "Creation date cannot be after last edited date" +msgstr "" + #: src/strings.ts:2051 msgid "Credentials" msgstr "" @@ -2259,11 +2260,11 @@ msgstr "" #: src/strings.ts:1078 msgid "Delete account" -msgstr "<<<<<<< HEAD=======" +msgstr "" #: src/strings.ts:2755 msgid "Delete all" -msgstr ">>>>>>> 101851125 (mobile: add failed inbox item history)" +msgstr "" #: src/strings.ts:2732 msgid "Delete attachment" @@ -2678,7 +2679,7 @@ msgstr "" #: src/strings.ts:1659 msgid "Encrypted, private, secure." -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2762 msgid "Encrypting" @@ -2892,6 +2893,18 @@ msgstr "" msgid "Expiry date" msgstr "" +#: src/strings.ts:2773 +msgid "Expiry date cannot be more than 1 year in the future" +msgstr "" + +#: src/strings.ts:2771 +msgid "Expiry date must be in the future" +msgstr "" + +#: src/strings.ts:2774 +msgid "Expiry date set" +msgstr "" + #: src/strings.ts:2552 msgid "Explore all plans" msgstr "" @@ -3081,7 +3094,7 @@ msgstr "" #: src/strings.ts:803 msgid "File length mismatch. Expected {expectedSize} but got {currentSize} bytes. Please upload this file again from the attachment manager." -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2770 msgid "File links cannot be opened in browsers. Please use the Notesnook desktop app." @@ -4111,6 +4124,10 @@ msgstr "" msgid "Maximize" msgstr "" +#: src/strings.ts:2779 +msgid "Maximum reminder date is {maxDate}" +msgstr "" + #: src/strings.ts:2082 msgid "Meet other privacy-minded people & talk to us directly about your concerns, issues and suggestions." msgstr "" @@ -4486,6 +4503,10 @@ msgstr "" msgid "Note does not exist" msgstr "" +#: src/strings.ts:2777 +msgid "Note duplicated" +msgstr "" + #: src/strings.ts:458 msgid "Note history" msgstr "" @@ -4714,7 +4735,7 @@ msgstr "" #: src/strings.ts:985 msgid "Open the two-factor authentication (TOTP) app to view your authentication code." -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2766 msgid "Opening local file" @@ -4841,6 +4862,10 @@ msgstr "" msgid "PDF is password protected" msgstr "" +#: src/strings.ts:2786 +msgid "Permission required to save QR-Code to Gallery" +msgstr "" + #: src/strings.ts:1731 msgid "phone number" msgstr "" @@ -5220,11 +5245,11 @@ msgstr "" #: src/strings.ts:2193 msgid "Proxy" -msgstr "<<<<<<< HEAD" +msgstr "" #: src/strings.ts:2746 msgid "Public key required" -msgstr "=======>>>>>>> 101851125 (mobile: add failed inbox item history)" +msgstr "" #: src/strings.ts:2712 msgid "Public Key:" @@ -7472,6 +7497,14 @@ msgstr "" 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 "" +#: src/strings.ts:2781 +msgid "We couldn't load this theme. Please make sure the file is a valid JSON theme file." +msgstr "" + +#: src/strings.ts:2783 +msgid "We couldn't load this theme. The file appears to be incomplete or missing required theme properties." +msgstr "" + #: src/strings.ts:1392 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 "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index ec2f62180..e7181e3ea 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2767,5 +2767,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` };