mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
web: add feature limits in almost all places
This commit is contained in:
@@ -31,22 +31,18 @@ import {
|
||||
import { introduceFeatures, showUpgradeReminderDialogs } from "./common";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { db } from "./common/db";
|
||||
import { CHECK_IDS, EV, EVENTS } from "@notesnook/core";
|
||||
import { EV, EVENTS } from "@notesnook/core";
|
||||
import { registerKeyMap } from "./common/key-map";
|
||||
import { isUserPremium } from "./hooks/use-is-user-premium";
|
||||
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
||||
import { showToast } from "./utils/toast";
|
||||
import {
|
||||
interruptedOnboarding,
|
||||
OnboardingDialog
|
||||
} from "./dialogs/onboarding-dialog";
|
||||
import { hashNavigate } from "./navigation";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
import { BuyDialog } from "./dialogs/buy-dialog";
|
||||
import { FeatureDialog } from "./dialogs/feature-dialog";
|
||||
import { AnnouncementDialog } from "./dialogs/announcement-dialog";
|
||||
import { logger } from "./utils/logger";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export default function AppEffects() {
|
||||
const refreshNavItems = useStore((store) => store.refreshNavItems);
|
||||
@@ -63,38 +59,6 @@ export default function AppEffects() {
|
||||
|
||||
useEffect(
|
||||
function initializeApp() {
|
||||
const userCheckStatusEvent = EV.subscribe(
|
||||
EVENTS.userCheckStatus,
|
||||
async (type: string) => {
|
||||
if (isUserPremium()) {
|
||||
return { type, result: true };
|
||||
} else {
|
||||
let sentence;
|
||||
switch (type) {
|
||||
case CHECK_IDS.noteColor:
|
||||
sentence = strings.upgradeToProToUseFeature("color");
|
||||
break;
|
||||
case CHECK_IDS.noteTag:
|
||||
sentence = strings.upgradeToProToUseFeature("tags");
|
||||
break;
|
||||
case CHECK_IDS.notebookAdd:
|
||||
sentence = strings.upgradeToProToUseFeature("notebook");
|
||||
break;
|
||||
case CHECK_IDS.vaultAdd:
|
||||
sentence = strings.upgradeToProToUseFeature("vault");
|
||||
break;
|
||||
default:
|
||||
sentence = strings.upgradeToProToUseFeature();
|
||||
break;
|
||||
}
|
||||
showToast("error", sentence, [
|
||||
{ text: strings.upgradeNow(), onClick: () => BuyDialog.show({}) }
|
||||
]);
|
||||
return { type, result: false };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
initStore();
|
||||
initEditorStore();
|
||||
|
||||
@@ -116,10 +80,6 @@ export default function AppEffects() {
|
||||
// NOTE: we deliberately don't await here because we don't want to pause execution.
|
||||
db.attachments.cacheAttachments().catch(logger.error);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
userCheckStatusEvent.unsubscribe();
|
||||
};
|
||||
},
|
||||
[
|
||||
initEditorStore,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { store as appStore } from "../stores/app-store";
|
||||
import { Backup, User, Email, Warn, Icon } from "../components/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { hardNavigate, hashNavigate } from "../navigation";
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { TaskScheduler } from "../utils/task-scheduler";
|
||||
import { BuyDialog } from "../dialogs/buy-dialog";
|
||||
@@ -91,12 +90,7 @@ export async function shouldAddAutoBackupsDisabledNotice() {
|
||||
if (!user) return false;
|
||||
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
if (!isUserPremium(user) && backupInterval) {
|
||||
Config.set("backupReminderOffset", 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return backupInterval === 0;
|
||||
}
|
||||
|
||||
export async function shouldAddBackupNotice() {
|
||||
@@ -206,7 +200,7 @@ let openedToast: { hide: () => void } | null = null;
|
||||
async function saveBackup(mode: "full" | "partial" = "partial") {
|
||||
if (IS_DESKTOP_APP) {
|
||||
await createBackup({ noVerify: true, mode, background: true });
|
||||
} else if (isUserPremium() && !IS_TESTING) {
|
||||
} else if (!IS_TESTING) {
|
||||
if (openedToast !== null) return;
|
||||
openedToast = showToast(
|
||||
"success",
|
||||
|
||||
@@ -32,6 +32,8 @@ import { MenuItem } from "@notesnook/ui";
|
||||
import { navigate } from "../../navigation";
|
||||
import { Tag } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
type HeaderProps = { readonly: boolean; id: string };
|
||||
function Header(props: HeaderProps) {
|
||||
@@ -51,9 +53,12 @@ function Header(props: HeaderProps) {
|
||||
if (oldTag) {
|
||||
await db.relations.unlink(oldTag, { type: "note", id: noteId });
|
||||
} else {
|
||||
const id =
|
||||
(await db.tags.find(value))?.id ??
|
||||
(await db.tags.add({ title: value }));
|
||||
let id = (await db.tags.find(value))?.id;
|
||||
if (!id) {
|
||||
const result = await isFeatureAvailable("tags");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
id = await db.tags.add({ title: value });
|
||||
}
|
||||
if (!id) return;
|
||||
await db.relations.add({ id, type: "tag" }, { type: "note", id: noteId });
|
||||
await useTagStore.getState().refresh();
|
||||
|
||||
@@ -21,12 +21,10 @@ import { SerializedKey } from "@notesnook/crypto";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { db } from "../../common/db";
|
||||
import { TaskManager } from "../../common/task-manager";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { showFilePicker } from "../../utils/file-picker";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
import { ImagePickerDialog } from "../../dialogs/image-picker-dialog";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
getUploadedFileSize,
|
||||
@@ -36,16 +34,9 @@ import {
|
||||
import Config from "../../utils/config";
|
||||
import { compressImage, FileWithURI } from "../../utils/image-compressor";
|
||||
import { ImageCompressionOptions } from "../../stores/setting-store";
|
||||
|
||||
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
|
||||
export async function insertAttachments(type = "*/*") {
|
||||
if (!isUserPremium()) {
|
||||
await BuyDialog.show({});
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await showFilePicker({
|
||||
acceptedFileTypes: type || "*/*",
|
||||
multiple: true
|
||||
@@ -55,11 +46,6 @@ export async function insertAttachments(type = "*/*") {
|
||||
}
|
||||
|
||||
export async function attachFiles(files: File[]) {
|
||||
if (!isUserPremium()) {
|
||||
await BuyDialog.show({});
|
||||
return;
|
||||
}
|
||||
|
||||
let images = files.filter((f) => f.type.startsWith("image/"));
|
||||
const imageCompressionConfig = Config.get<ImageCompressionOptions>(
|
||||
"imageCompression",
|
||||
@@ -68,7 +54,7 @@ export async function attachFiles(files: File[]) {
|
||||
|
||||
switch (imageCompressionConfig) {
|
||||
case ImageCompressionOptions.ENABLE: {
|
||||
let compressedImages: FileWithURI[] = [];
|
||||
const compressedImages: FileWithURI[] = [];
|
||||
for (const image of images) {
|
||||
const compressed = await compressImage(image, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
@@ -143,8 +129,8 @@ async function pickFile(
|
||||
options?: AddAttachmentOptions
|
||||
): Promise<Attachment | undefined> {
|
||||
try {
|
||||
if (file.size > FILE_SIZE_LIMIT)
|
||||
throw new Error(strings.fileTooLargeDesc(500));
|
||||
const feature = await isFeatureAvailable("fileSize", file.size);
|
||||
if (!feature.isAllowed) throw new Error(feature.error);
|
||||
|
||||
const hash = await addAttachment(file, options);
|
||||
return {
|
||||
@@ -169,9 +155,8 @@ async function pickImage(
|
||||
options?: AddAttachmentOptions
|
||||
): Promise<Attachment | undefined> {
|
||||
try {
|
||||
if (file.size > IMAGE_SIZE_LIMIT)
|
||||
throw new Error(strings.imageTooLarge(50));
|
||||
if (!file) return;
|
||||
const feature = await isFeatureAvailable("fileSize", file.size);
|
||||
if (!feature.isAllowed) throw new Error(feature.error);
|
||||
|
||||
const hash = await addAttachment(file, options);
|
||||
const dimensions = await getImageDimensions(file);
|
||||
|
||||
@@ -50,9 +50,12 @@ import {
|
||||
} from "react";
|
||||
import { IEditor, MAX_AUTO_SAVEABLE_WORDS } from "./types";
|
||||
import { useEditorConfig, useToolbarConfig, useEditorManager } from "./manager";
|
||||
import { useIsUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { debounce } from "@notesnook/common";
|
||||
import {
|
||||
debounce,
|
||||
useAreFeaturesAvailable,
|
||||
useIsFeatureAvailable
|
||||
} from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
@@ -65,6 +68,7 @@ import { TimeFormat } from "@notesnook/core";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog";
|
||||
import { EDITOR_ZOOM } from "./common";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -183,16 +187,31 @@ function TipTap(props: TipTapProps) {
|
||||
fontLigatures
|
||||
} = props;
|
||||
|
||||
const isUserPremium = useIsUserPremium();
|
||||
const autoSave = useRef(true);
|
||||
const { toolbarConfig } = useToolbarConfig();
|
||||
|
||||
const features = useAreFeaturesAvailable([
|
||||
"callout",
|
||||
"outlineList",
|
||||
"taskList"
|
||||
]);
|
||||
|
||||
usePermissionHandler({
|
||||
claims: {
|
||||
premium: isUserPremium
|
||||
callout: !!features?.callout?.isAllowed,
|
||||
outlineList: !!features?.outlineList?.isAllowed,
|
||||
taskList: !!features?.taskList?.isAllowed
|
||||
},
|
||||
onPermissionDenied: (claim) => {
|
||||
if (claim === "premium") BuyDialog.show({});
|
||||
onPermissionDenied: (claim, silent) => {
|
||||
if (silent) {
|
||||
console.log(features, features?.[claim]);
|
||||
if (features?.[claim]) showFeatureNotAllowedToast(features[claim]);
|
||||
return;
|
||||
}
|
||||
|
||||
BuyDialog.show({
|
||||
plan: features?.[claim]?.availableOn
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -107,6 +107,8 @@ import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||
import { useStore as useTagStore } from "../../stores/tag-store";
|
||||
import { showSortMenu } from "../group-header";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog";
|
||||
import { useIsFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
type Route = {
|
||||
id: "notes" | "favorites" | "reminders" | "monographs" | "trash" | "archive";
|
||||
@@ -902,6 +904,7 @@ function ReorderableList<T extends { id: string }>(
|
||||
const [activeItem, setActiveItem] = useState<T>();
|
||||
const [order, setOrder] = usePersistentState<string[]>(orderKey, _order());
|
||||
const orderedItems = orderItems(items, order);
|
||||
const customizableSidebar = useIsFeatureAvailable("customizableSidebar");
|
||||
|
||||
useEffect(() => {
|
||||
setOrder(_order());
|
||||
@@ -912,10 +915,10 @@ function ReorderableList<T extends { id: string }>(
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
cancelDrop={() => {
|
||||
// if (!isUserPremium()) {
|
||||
// showToast("error", "You need to be Pro to customize the sidebar.");
|
||||
// return true;
|
||||
// }
|
||||
if (!customizableSidebar?.isAllowed) {
|
||||
showFeatureNotAllowedToast(customizableSidebar);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
onDragStart={(event) => {
|
||||
|
||||
@@ -47,7 +47,6 @@ import { AddTagsDialog } from "../../dialogs/add-tags-dialog";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { CreateColorDialog } from "../../dialogs/create-color-dialog";
|
||||
import { MoveNoteDialog } from "../../dialogs/move-note-dialog";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { navigate } from "../../navigation";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { useStore as useMonographStore } from "../../stores/monograph-store";
|
||||
@@ -332,7 +331,6 @@ export const noteMenuItems: (
|
||||
ids?: string[],
|
||||
context?: { color?: Color; locked?: boolean }
|
||||
) => MenuItem[] = (note, ids = [], context) => {
|
||||
const isPro = isUserPremium();
|
||||
// const isSynced = db.notes.note(note.id)?.synced();
|
||||
|
||||
return [
|
||||
@@ -377,7 +375,6 @@ export const noteMenuItems: (
|
||||
//isDisabled: !isSynced,
|
||||
title: strings.lock(),
|
||||
isChecked: context?.locked,
|
||||
isDisabled: !isPro,
|
||||
icon: Lock.path,
|
||||
onClick: async () => {
|
||||
const { unlock, lock } = store.get();
|
||||
@@ -517,11 +514,7 @@ export const noteMenuItems: (
|
||||
title: format.title,
|
||||
tooltip: strings.exportAs(format.title),
|
||||
icon: format.icon.path,
|
||||
isDisabled:
|
||||
(format.type !== "txt" && !isPro) ||
|
||||
(format.type === "pdf" && ids.length > 1),
|
||||
// ? "Multiple notes cannot be exported as PDF."
|
||||
// : false,
|
||||
isDisabled: format.type === "pdf" && ids.length > 1,
|
||||
multiSelect: true,
|
||||
onClick: async () => {
|
||||
if (ids.length === 1) {
|
||||
|
||||
@@ -47,6 +47,8 @@ import { db } from "../../common/db";
|
||||
import { createSetDefaultHomepageMenuItem } from "../../common";
|
||||
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||
import { MoveNotebookDialog } from "../../dialogs/move-notebook-dialog";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
type NotebookProps = {
|
||||
item: NotebookType;
|
||||
@@ -210,6 +212,10 @@ export const notebookMenuItems: (
|
||||
onClick: async () => {
|
||||
const defaultNotebook = db.settings.getDefaultNotebook();
|
||||
const isDefault = defaultNotebook === notebook.id;
|
||||
if (!isDefault) {
|
||||
const result = await isFeatureAvailable("defaultNotebookAndTag");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
}
|
||||
await db.settings.setDefaultNotebook(
|
||||
isDefault ? undefined : notebook.id
|
||||
);
|
||||
|
||||
@@ -32,6 +32,8 @@ import { useStore as useNoteStore } from "../../stores/note-store";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { createSetDefaultHomepageMenuItem } from "../../common";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
type TagProps = { item: TagType; totalNotes: number };
|
||||
function Tag(props: TagProps) {
|
||||
@@ -123,6 +125,10 @@ export const tagMenuItems: (tag: TagType, ids?: string[]) => MenuItem[] = (
|
||||
onClick: async () => {
|
||||
const defaultTag = db.settings.getDefaultTag();
|
||||
const isDefault = defaultTag === tag.id;
|
||||
if (!isDefault) {
|
||||
const result = await isFeatureAvailable("defaultNotebookAndTag");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
}
|
||||
await db.settings.setDefaultTag(isDefault ? undefined : tag.id);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,10 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import Tip from "../tip";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { Flex, Switch } from "@theme-ui/components";
|
||||
import { Loading } from "../icons";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog/buy-dialog";
|
||||
|
||||
function Toggle(props) {
|
||||
const {
|
||||
@@ -32,24 +30,21 @@ function Toggle(props) {
|
||||
isToggled,
|
||||
onToggled,
|
||||
onlyIf,
|
||||
premium,
|
||||
testId,
|
||||
disabled,
|
||||
tip
|
||||
} = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const onClick = useCallback(async () => {
|
||||
if (isUserPremium() || !premium || isToggled) {
|
||||
if (isToggled) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onToggled();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
await BuyDialog.show({});
|
||||
}
|
||||
}, [onToggled, premium, isToggled]);
|
||||
}, [onToggled, isToggled]);
|
||||
|
||||
if (onlyIf === false) return null;
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,8 @@ import { store as appStore } from "../stores/app-store";
|
||||
import { db } from "../common/db";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
type AddNotebookDialogProps = BaseDialogProps<boolean> & {
|
||||
parentId?: string;
|
||||
@@ -45,6 +47,11 @@ export const AddNotebookDialog = DialogManager.register(
|
||||
if (!title.current.trim())
|
||||
return showToast("error", strings.allFieldsRequired());
|
||||
|
||||
const result = await isFeatureAvailable("notebooks");
|
||||
if (!result.isAllowed) {
|
||||
return showFeatureNotAllowedToast(result);
|
||||
}
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
id: props.notebook?.id,
|
||||
title: title.current,
|
||||
|
||||
@@ -26,17 +26,21 @@ import { useRef, useState } from "react";
|
||||
import { db } from "../common/db";
|
||||
import { useStore } from "../stores/reminder-store";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { useIsUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { Calendar, Pro } from "../components/icons";
|
||||
import { usePersistentState } from "../hooks/use-persistent-state";
|
||||
import { DayPicker } from "../components/day-picker";
|
||||
import { PopupPresenter } from "@notesnook/ui";
|
||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import {
|
||||
isFeatureAvailable,
|
||||
getFormattedDate,
|
||||
useIsFeatureAvailable
|
||||
} from "@notesnook/common";
|
||||
import { MONTHS_FULL, getTimeFormat } from "@notesnook/core";
|
||||
import { Note, Reminder } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
@@ -74,8 +78,7 @@ const modes = [
|
||||
},
|
||||
{
|
||||
id: Modes.REPEAT,
|
||||
title: "Repeat",
|
||||
premium: true
|
||||
title: "Repeat"
|
||||
}
|
||||
];
|
||||
const priorities = [
|
||||
@@ -140,9 +143,9 @@ export const AddReminderDialog = DialogManager.register(
|
||||
);
|
||||
const [showCalendar, setShowCalendar] = useState(false);
|
||||
const refresh = useStore((state) => state.refresh);
|
||||
const isUserPremium = useIsUserPremium();
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const dateInputRef = useRef<HTMLInputElement>(null);
|
||||
const repeatModeAvailability = useIsFeatureAvailable("recurringReminders");
|
||||
|
||||
const repeatsDaily =
|
||||
(selectedDays.length === 7 && recurringMode === RecurringModes.WEEK) ||
|
||||
@@ -180,6 +183,12 @@ export const AddReminderDialog = DialogManager.register(
|
||||
return;
|
||||
}
|
||||
|
||||
const feature = await isFeatureAvailable("activeReminders");
|
||||
if (!feature.isAllowed) {
|
||||
showFeatureNotAllowedToast(feature);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = await db.reminders.add({
|
||||
id: reminder?.id,
|
||||
recurringMode,
|
||||
@@ -249,17 +258,18 @@ export const AddReminderDialog = DialogManager.register(
|
||||
name="mode"
|
||||
defaultChecked={m.id === Modes.ONCE}
|
||||
checked={m.id === mode}
|
||||
disabled={m.premium && !isUserPremium}
|
||||
disabled={
|
||||
m.id === "repeat" && !repeatModeAvailability?.isAllowed
|
||||
}
|
||||
sx={{ color: m.id === mode ? "accent" : "icon" }}
|
||||
onChange={() => {
|
||||
if (m.premium && !isUserPremium) return;
|
||||
onChange={async () => {
|
||||
setMode(m.id);
|
||||
setRecurringMode(RecurringModes.DAY);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
/>
|
||||
{strings.reminderModes(m.id)}
|
||||
{m.premium && !isUserPremium && (
|
||||
{m.id === "repeat" && !repeatModeAvailability?.isAllowed && (
|
||||
<Pro size={18} color="accent" sx={{ ml: 1 }} />
|
||||
)}
|
||||
</Label>
|
||||
|
||||
@@ -26,7 +26,7 @@ import { store as notestore } from "../stores/note-store";
|
||||
import { FilteredList } from "../components/filtered-list";
|
||||
import { ItemReference, Tag } from "@notesnook/core";
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { ResolvedItem } from "@notesnook/common";
|
||||
import { isFeatureAvailable, ResolvedItem } from "@notesnook/common";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
selectMultiple,
|
||||
useSelectionStore
|
||||
} from "./move-note-dialog";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
type AddTagsDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
||||
export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
|
||||
@@ -115,6 +116,11 @@ export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
|
||||
);
|
||||
}}
|
||||
onCreateNewItem={async (title) => {
|
||||
const result = await isFeatureAvailable("tags");
|
||||
if (!result.isAllowed) {
|
||||
return showFeatureNotAllowedToast(result);
|
||||
}
|
||||
|
||||
const tagId = await db.tags.add({ title });
|
||||
if (!tagId) return;
|
||||
await useStore.getState().refresh();
|
||||
|
||||
@@ -29,7 +29,7 @@ import Field from "../../components/field";
|
||||
import { hardNavigate } from "../../navigation";
|
||||
import { Features } from "./features";
|
||||
import { PaddleCheckout } from "./paddle";
|
||||
import { Period, Plan, PlanId, Price, PricingInfo } from "./types";
|
||||
import { Period, Plan, Price, PricingInfo } from "./types";
|
||||
import { usePlans } from "./plans";
|
||||
import {
|
||||
formatRecurringPeriodShort,
|
||||
@@ -46,13 +46,13 @@ import { isUserSubscribed } from "../../hooks/use-is-user-premium";
|
||||
import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
||||
import BaseDialog from "../../components/dialog";
|
||||
import { ScopedThemeProvider } from "../../components/theme-provider";
|
||||
import { User } from "@notesnook/core";
|
||||
import { SubscriptionPlan, User } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type BuyDialogProps = BaseDialogProps<false> & {
|
||||
couponCode?: string;
|
||||
plan?: PlanId;
|
||||
plan?: SubscriptionPlan;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
@@ -115,7 +115,7 @@ export const BuyDialog = DialogManager.register(function BuyDialog(
|
||||
>
|
||||
<CheckoutSideBar
|
||||
onClose={() => onClose(false)}
|
||||
initialPlan={plan || "free"}
|
||||
initialPlan={plan || SubscriptionPlan.FREE}
|
||||
user={user}
|
||||
/>
|
||||
</ScopedThemeProvider>
|
||||
@@ -126,7 +126,7 @@ export const BuyDialog = DialogManager.register(function BuyDialog(
|
||||
});
|
||||
|
||||
type SideBarProps = {
|
||||
initialPlan: PlanId;
|
||||
initialPlan: SubscriptionPlan;
|
||||
onClose: () => void;
|
||||
user?: User;
|
||||
};
|
||||
@@ -171,7 +171,7 @@ export function CheckoutSideBar(props: SideBarProps) {
|
||||
if (user)
|
||||
return (
|
||||
<PlansList
|
||||
selectedPlan={selectedPlan?.id || initialPlan || "free"}
|
||||
selectedPlan={selectedPlan?.id || initialPlan}
|
||||
onPlansLoaded={(plans) => {
|
||||
// if (!initialPlan || showPlans) return;
|
||||
// const plan = plans.find((p) => p.id === initialPlan);
|
||||
@@ -476,7 +476,7 @@ function SelectedPlan(props: SelectedPlanProps) {
|
||||
>
|
||||
{plan.title}
|
||||
</Text>
|
||||
{plan.id === "education" && (
|
||||
{plan.id === SubscriptionPlan.EDUCATION && (
|
||||
<Link
|
||||
href="https://notesnook.com/education"
|
||||
target="_blank"
|
||||
|
||||
@@ -20,14 +20,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Text, Flex, Button, Image } from "@theme-ui/components";
|
||||
import { Loading } from "../../components/icons";
|
||||
import Nomad from "../../assets/nomad.svg?url";
|
||||
import { Period, Plan, PlanId, Price } from "./types";
|
||||
import { Period, Plan, Price } from "./types";
|
||||
import { usePlans } from "./plans";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getCurrencySymbol, parseAmount } from "./helpers";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { SubscriptionPlan } from "@notesnook/core";
|
||||
|
||||
type PlansListProps = {
|
||||
selectedPlan: PlanId;
|
||||
selectedPlan: SubscriptionPlan;
|
||||
onPlanSelected: (plan: Plan, price: Price) => void;
|
||||
onPlansLoaded?: (plans: Plan[]) => void;
|
||||
};
|
||||
|
||||
@@ -20,20 +20,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useEffect, useState } from "react";
|
||||
import { Period, Plan, Price } from "./types";
|
||||
import { IS_DEV } from "./helpers";
|
||||
import { SubscriptionPlan } from "@notesnook/core";
|
||||
|
||||
function createPrice(id: string, period: Period, subtotal: number): Price {
|
||||
return {
|
||||
id,
|
||||
period,
|
||||
subtotal,
|
||||
total: 0,
|
||||
tax: 0,
|
||||
subtotal: `${subtotal}USD`,
|
||||
total: `0.00USD`,
|
||||
tax: `0.00USD`,
|
||||
currency: "USD"
|
||||
};
|
||||
}
|
||||
|
||||
const FREE_PLAN: Plan = {
|
||||
id: "free",
|
||||
id: SubscriptionPlan.FREE,
|
||||
title: "Free",
|
||||
recurring: true,
|
||||
prices: [
|
||||
@@ -46,7 +47,7 @@ const FREE_PLAN: Plan = {
|
||||
export const DEFAULT_PLANS: Plan[] = [
|
||||
FREE_PLAN,
|
||||
{
|
||||
id: "essential",
|
||||
id: SubscriptionPlan.ESSENTIAL,
|
||||
title: "Essential",
|
||||
recurring: true,
|
||||
prices: [
|
||||
@@ -67,7 +68,7 @@ export const DEFAULT_PLANS: Plan[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
id: SubscriptionPlan.PRO,
|
||||
title: "Pro",
|
||||
recurring: true,
|
||||
prices: [
|
||||
@@ -95,7 +96,7 @@ export const DEFAULT_PLANS: Plan[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "believer",
|
||||
id: SubscriptionPlan.BELIEVER,
|
||||
title: "Believer",
|
||||
recurring: true,
|
||||
prices: [
|
||||
@@ -123,7 +124,7 @@ export const DEFAULT_PLANS: Plan[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "education",
|
||||
id: SubscriptionPlan.EDUCATION,
|
||||
title: "Education",
|
||||
recurring: false,
|
||||
prices: [
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { SubscriptionPlan } from "@notesnook/core";
|
||||
import {
|
||||
CheckoutEventNames,
|
||||
CheckoutEventsCustomer,
|
||||
@@ -51,10 +52,9 @@ export type PaddleEvent = {
|
||||
callback_data: PaddleEventData;
|
||||
};
|
||||
|
||||
export type PlanId = "free" | "essential" | "pro" | "believer" | "education";
|
||||
export interface Plan {
|
||||
// period: Period;
|
||||
id: PlanId;
|
||||
id: SubscriptionPlan;
|
||||
title: string;
|
||||
prices: Price[];
|
||||
recurring: boolean;
|
||||
|
||||
@@ -25,6 +25,8 @@ import { db } from "../common/db";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
type CreateColorDialogProps = BaseDialogProps<string | false>;
|
||||
export const CreateColorDialog = DialogManager.register(
|
||||
@@ -59,6 +61,10 @@ export const CreateColorDialog = DialogManager.register(
|
||||
showToast("error", strings.invalidHexColor());
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await isFeatureAvailable("colors");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
|
||||
const colorId = await db.colors.add({
|
||||
colorCode: form.color,
|
||||
title: form.title
|
||||
|
||||
@@ -21,10 +21,11 @@ import { useEffect, useState } from "react";
|
||||
import Dialog from "../components/dialog";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Flex, Image, Label, Text } from "@theme-ui/components";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { formatBytes, isFeatureAvailable } from "@notesnook/common";
|
||||
import { compressImage, FileWithURI } from "../utils/image-compressor";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
export type ImagePickerDialogProps = BaseDialogProps<false | File[]> & {
|
||||
images: File[];
|
||||
@@ -130,7 +131,11 @@ export const ImagePickerDialog = DialogManager.register(
|
||||
}}
|
||||
defaultChecked={compress}
|
||||
checked={compress}
|
||||
onChange={() => setCompress((s) => !s)}
|
||||
onChange={async () => {
|
||||
const result = await isFeatureAvailable("fullQualityImages");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
setCompress((s) => !s);
|
||||
}}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>
|
||||
Enable compression (recommended)
|
||||
|
||||
@@ -30,7 +30,6 @@ import { ConfirmDialog } from "./confirm";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { getDeviceInfo } from "../utils/platform";
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
|
||||
const PLACEHOLDERS = {
|
||||
title: strings.issueTitlePlaceholder(),
|
||||
|
||||
@@ -30,6 +30,8 @@ import { useStore as useNoteStore } from "../stores/note-store";
|
||||
import { useStore as useAppStore } from "../stores/app-store";
|
||||
import { Color, Tag } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
type ItemDialogProps = BaseDialogProps<false | string> & {
|
||||
title: string;
|
||||
@@ -97,6 +99,11 @@ export const CreateTagDialog = {
|
||||
title: strings.addTag(),
|
||||
subtitle: strings.addTagDesc()
|
||||
}).then(async (title) => {
|
||||
const result = await isFeatureAvailable("tags");
|
||||
if (!result.isAllowed) {
|
||||
return showFeatureNotAllowedToast(result);
|
||||
}
|
||||
|
||||
if (
|
||||
!title ||
|
||||
!(await db.tags.add({ title }).catch((e) => {
|
||||
|
||||
@@ -80,7 +80,6 @@ export const BackupExportSettings: SettingsGroup[] = [
|
||||
key: "auto-backup",
|
||||
title: strings.automaticBackups(),
|
||||
description: strings.automaticBackupsDesc(),
|
||||
// isHidden: () => !isUserPremium(),
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.backupReminderOffset, listener),
|
||||
components: [
|
||||
|
||||
@@ -24,18 +24,18 @@ import {
|
||||
useStore as useSettingStore
|
||||
} from "../../stores/setting-store";
|
||||
import dayjs from "dayjs";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { TimeFormat } from "@notesnook/core";
|
||||
import { TrashCleanupInterval } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { BuyDialog } from "../buy-dialog";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
export const BehaviourSettings: SettingsGroup[] = [
|
||||
{
|
||||
key: "general",
|
||||
section: "behaviour",
|
||||
header: strings.general(),
|
||||
isHidden: () => !isUserPremium(),
|
||||
settings: [
|
||||
{
|
||||
key: "default-sidebar-tab",
|
||||
@@ -47,9 +47,12 @@ export const BehaviourSettings: SettingsGroup[] = [
|
||||
components: [
|
||||
{
|
||||
type: "dropdown",
|
||||
onSelectionChanged: (value) => {
|
||||
if (!isUserPremium()) {
|
||||
BuyDialog.show({});
|
||||
onSelectionChanged: async (value) => {
|
||||
const defaultSidebarTab = await isFeatureAvailable(
|
||||
"defaultSidebarTab"
|
||||
);
|
||||
if (!defaultSidebarTab.isAllowed) {
|
||||
BuyDialog.show({ plan: defaultSidebarTab.availableOn });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,8 +77,15 @@ export const BehaviourSettings: SettingsGroup[] = [
|
||||
components: [
|
||||
{
|
||||
type: "dropdown",
|
||||
onSelectionChanged: (value) =>
|
||||
useSettingStore.getState().setImageCompression(parseInt(value)),
|
||||
onSelectionChanged: async (value) => {
|
||||
if (value === ImageCompressionOptions.DISABLE.toString()) {
|
||||
const result = await isFeatureAvailable("fullQualityImages");
|
||||
if (!result.isAllowed)
|
||||
return showFeatureNotAllowedToast(result);
|
||||
}
|
||||
|
||||
useSettingStore.getState().setImageCompression(parseInt(value));
|
||||
},
|
||||
selectedOption: () =>
|
||||
useSettingStore.getState().imageCompression.toString(),
|
||||
options: [
|
||||
@@ -159,9 +169,12 @@ export const BehaviourSettings: SettingsGroup[] = [
|
||||
components: [
|
||||
{
|
||||
type: "dropdown",
|
||||
onSelectionChanged: (value) => {
|
||||
if (!isUserPremium()) {
|
||||
BuyDialog.show({});
|
||||
onSelectionChanged: async (value) => {
|
||||
const disableTrashCleanup = await isFeatureAvailable(
|
||||
"disableTrashCleanup"
|
||||
);
|
||||
if (value === "-1" && !disableTrashCleanup.isAllowed) {
|
||||
BuyDialog.show({ plan: disableTrashCleanup.availableOn });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +191,7 @@ export const BehaviourSettings: SettingsGroup[] = [
|
||||
{ value: "7", title: strings.days(7) },
|
||||
{ value: "30", title: strings.days(30) },
|
||||
{ value: "365", title: strings.days(365) },
|
||||
{ value: "-1", title: strings.never(), premium: true }
|
||||
{ value: "-1", title: strings.never() }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -59,12 +59,14 @@ import {
|
||||
PresetId
|
||||
} from "../../../common/toolbar-config";
|
||||
import { showToast } from "../../../utils/toast";
|
||||
import { isUserPremium } from "../../../hooks/use-is-user-premium";
|
||||
import { Pro } from "../../../components/icons";
|
||||
|
||||
import { Icon } from "@notesnook/ui";
|
||||
import { CURRENT_TOOLBAR_VERSION } from "@notesnook/common";
|
||||
import {
|
||||
CURRENT_TOOLBAR_VERSION,
|
||||
useIsFeatureAvailable
|
||||
} from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { showFeatureNotAllowedToast } from "../../../common/toasts";
|
||||
|
||||
export function CustomizeToolbar() {
|
||||
const sensors = useSensors(
|
||||
@@ -77,6 +79,7 @@ export function CustomizeToolbar() {
|
||||
const [activeItem, setActiveItem] = useState<TreeNode>();
|
||||
const [currentPreset, setCurrentPreset] = useState<Preset>();
|
||||
const { setToolbarConfig } = useToolbarConfig();
|
||||
const customToolbarPreset = useIsFeatureAvailable("customToolbarPreset");
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPreset) return;
|
||||
@@ -131,18 +134,12 @@ export function CustomizeToolbar() {
|
||||
value={preset.id}
|
||||
checked={preset.id === currentPreset.id}
|
||||
defaultChecked={preset.id === currentPreset.id}
|
||||
disabled={preset.id === "custom" && !isUserPremium()}
|
||||
disabled={
|
||||
preset.id === "custom" && !customToolbarPreset?.isAllowed
|
||||
}
|
||||
style={{ accentColor: "var(--accent)" }}
|
||||
onChange={async (e) => {
|
||||
const { value } = e.target;
|
||||
if (preset.id === "custom" && !isUserPremium()) {
|
||||
showToast(
|
||||
"info",
|
||||
strings.upgradeToProToUseFeature("customPresets")
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log("CHANGE PRESET", value);
|
||||
setCurrentPreset(getPreset(value as PresetId));
|
||||
}}
|
||||
/>
|
||||
@@ -155,7 +152,7 @@ export function CustomizeToolbar() {
|
||||
>
|
||||
{preset.title}
|
||||
</span>
|
||||
{preset.id === "custom" && !isUserPremium() ? (
|
||||
{preset.id === "custom" && !customToolbarPreset?.isAllowed ? (
|
||||
<Pro color="accent" size={18} sx={{ ml: 1 }} />
|
||||
) : null}
|
||||
</Label>
|
||||
@@ -185,11 +182,8 @@ export function CustomizeToolbar() {
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
cancelDrop={() => {
|
||||
if (!isUserPremium()) {
|
||||
showToast(
|
||||
"error",
|
||||
strings.upgradeToProToUseFeature("customizeToolbar")
|
||||
);
|
||||
if (!customToolbarPreset?.isAllowed) {
|
||||
showFeatureNotAllowedToast(customToolbarPreset);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -32,6 +32,8 @@ import { CustomizeToolbar } from "./components/customize-toolbar";
|
||||
import { DictionaryWords } from "./components/dictionary-words";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { isMac } from "../../utils/platform";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
|
||||
export const EditorSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -119,7 +121,11 @@ export const EditorSettings: SettingsGroup[] = [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () => useSettingStore.getState().markdownShortcuts,
|
||||
toggle: () => useSettingStore.getState().toggleMarkdownShortcuts()
|
||||
toggle: async () => {
|
||||
const result = await isFeatureAvailable("markdownShortcuts");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
useSettingStore.getState().toggleMarkdownShortcuts();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -133,7 +139,11 @@ export const EditorSettings: SettingsGroup[] = [
|
||||
{
|
||||
type: "toggle",
|
||||
isToggled: () => useSettingStore.getState().fontLigatures,
|
||||
toggle: () => useSettingStore.getState().toggleFontLigatures()
|
||||
toggle: async () => {
|
||||
const result = await isFeatureAvailable("fontLigatures");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
useSettingStore.getState().toggleFontLigatures();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
} from "./types";
|
||||
import { ProfileSettings } from "./profile-settings";
|
||||
import { AuthenticationSettings } from "./auth-settings";
|
||||
import { useIsUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { SyncSettings } from "./sync-settings";
|
||||
import { BehaviourSettings } from "./behaviour-settings";
|
||||
@@ -433,7 +432,6 @@ function SettingItem(props: { item: Setting }) {
|
||||
const { item } = props;
|
||||
const [state, setState] = useState<unknown>();
|
||||
const [workIndex, setWorkIndex] = useState<number>();
|
||||
const isUserPremium = useIsUserPremium();
|
||||
|
||||
useEffect(() => {
|
||||
if (!item.onStateChange) return;
|
||||
@@ -545,12 +543,7 @@ function SettingItem(props: { item: Setting }) {
|
||||
/>
|
||||
);
|
||||
case "dropdown":
|
||||
return (
|
||||
<SelectComponent
|
||||
{...component}
|
||||
isUserPremium={isUserPremium}
|
||||
/>
|
||||
);
|
||||
return <SelectComponent {...component} />;
|
||||
case "input":
|
||||
return component.inputType === "number" ? (
|
||||
<Input
|
||||
@@ -604,10 +597,8 @@ function SettingItem(props: { item: Setting }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SelectComponent(
|
||||
props: DropdownSettingComponent & { isUserPremium: boolean }
|
||||
) {
|
||||
const { onSelectionChanged, options, isUserPremium } = props;
|
||||
function SelectComponent(props: DropdownSettingComponent) {
|
||||
const { onSelectionChanged, options } = props;
|
||||
const selectedOption = usePromise(() => props.selectedOption(), [props]);
|
||||
|
||||
return (
|
||||
@@ -629,11 +620,7 @@ function SelectComponent(
|
||||
}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<option
|
||||
disabled={option.premium && !isUserPremium}
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.title}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -124,7 +124,7 @@ export type ToggleSettingComponent = BaseSettingComponent<"toggle"> & {
|
||||
};
|
||||
|
||||
export type DropdownSettingComponent = BaseSettingComponent<"dropdown"> & {
|
||||
options: { value: string | number; title: string; premium?: boolean }[];
|
||||
options: { value: string | number; title: string }[];
|
||||
selectedOption: () => string | number | Promise<string | number>;
|
||||
onSelectionChanged: (value: string) => void | Promise<void>;
|
||||
};
|
||||
|
||||
@@ -23,8 +23,6 @@ import { useStore as useNotesStore } from "../../stores/note-store";
|
||||
import Vault from "../../common/vault";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { db } from "../../common/db";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { BuyDialog } from "../buy-dialog/buy-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const VaultSettings: SettingsGroup[] = [
|
||||
@@ -44,11 +42,9 @@ export const VaultSettings: SettingsGroup[] = [
|
||||
type: "button",
|
||||
title: strings.create(),
|
||||
action: () => {
|
||||
if (!isUserPremium()) BuyDialog.show({});
|
||||
else
|
||||
Vault.createVault().then((res) => {
|
||||
useAppStore.getState().setIsVaultCreated(res);
|
||||
});
|
||||
Vault.createVault().then((res) => {
|
||||
useAppStore.getState().setIsVaultCreated(res);
|
||||
});
|
||||
},
|
||||
variant: "secondary"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ import { useEditorStore } from "./editor-store";
|
||||
import { useEditorManager } from "../components/editor/manager";
|
||||
import { exitFullscreen } from "../utils/fullscreen";
|
||||
import { NavigationTabItem } from "../components/navigation-menu";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { showFeatureNotAllowedToast } from "../common/toasts";
|
||||
|
||||
type SyncState =
|
||||
| "synced"
|
||||
@@ -251,11 +253,14 @@ class AppStore extends BaseStore<AppStore> {
|
||||
};
|
||||
|
||||
addToShortcuts = async (item: { type: "tag" | "notebook"; id: string }) => {
|
||||
if (await db.shortcuts.exists(item.id)) {
|
||||
if (db.shortcuts.exists(item.id)) {
|
||||
await db.shortcuts.remove(item.id);
|
||||
this.refreshNavItems();
|
||||
showToast("success", strings.shortcutRemoved());
|
||||
} else {
|
||||
const result = await isFeatureAvailable("shortcuts");
|
||||
if (!result.isAllowed) return showFeatureNotAllowedToast(result);
|
||||
|
||||
await db.shortcuts.add({
|
||||
itemType: item.type,
|
||||
itemId: item.id
|
||||
|
||||
@@ -36,7 +36,7 @@ const ToastIcons = {
|
||||
info: Info
|
||||
};
|
||||
|
||||
function showToast(
|
||||
export function showToast(
|
||||
type: ToastType,
|
||||
message: string,
|
||||
actions?: ToastAction[],
|
||||
@@ -102,8 +102,6 @@ function ToastContainer(props: ToastContainerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export { showToast };
|
||||
|
||||
function ToastIcon({ type }: { type: ToastType }) {
|
||||
const IconComponent = ToastIcons[type];
|
||||
return (
|
||||
|
||||
@@ -20,3 +20,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
export * from "./use-time-ago.js";
|
||||
export * from "./use-promise.js";
|
||||
export * from "./use-resolved-item.js";
|
||||
export * from "./use-is-feature-available.js";
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
ResolveInternalLink,
|
||||
isImage,
|
||||
isWebClip,
|
||||
CHECK_IDS,
|
||||
checkIsUserPremium,
|
||||
FilteredSelector,
|
||||
EMPTY_CONTENT
|
||||
} from "@notesnook/core";
|
||||
@@ -66,8 +64,6 @@ export async function* exportNotes(
|
||||
}
|
||||
) {
|
||||
const { format } = options;
|
||||
if (format !== "txt" && !(await checkIsUserPremium(CHECK_IDS.noteExport)))
|
||||
return;
|
||||
|
||||
const pathTree = new PathTree();
|
||||
const notePathMap: Map<string, string[]> = new Map();
|
||||
@@ -175,8 +171,6 @@ export async function* exportNote(
|
||||
}
|
||||
) {
|
||||
const { format } = options;
|
||||
if (format !== "txt" && !(await checkIsUserPremium(CHECK_IDS.noteExport)))
|
||||
return;
|
||||
|
||||
const attachmentsRoot = "attachments";
|
||||
const filename = sanitizeFilename(note.title || "Untitled", {
|
||||
|
||||
@@ -29,3 +29,4 @@ export * from "./export-notes.js";
|
||||
export * from "./dataurl.js";
|
||||
export * from "./tab-session-history.js";
|
||||
export * from "./keybindings.js";
|
||||
export * from "./is-feature-available.js";
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Cipher } from "@notesnook/crypto";
|
||||
import Database from "./index.js";
|
||||
import { CHECK_IDS, EV, EVENTS, checkIsUserPremium } from "../common.js";
|
||||
import { EV, EVENTS } from "../common.js";
|
||||
import { isCipher } from "../utils/crypto.js";
|
||||
import { Note, NoteContent } from "../types.js";
|
||||
import { logger } from "../logger.js";
|
||||
@@ -67,8 +67,6 @@ export default class Vault {
|
||||
}
|
||||
|
||||
async create(password: string) {
|
||||
if (!(await checkIsUserPremium(CHECK_IDS.vaultAdd))) return;
|
||||
|
||||
const vaultKey = await this.getKey();
|
||||
if (!vaultKey || !isCipher(vaultKey)) {
|
||||
const encryptedData = await this.db
|
||||
@@ -174,8 +172,6 @@ export default class Vault {
|
||||
* Locks (add to vault) a note
|
||||
*/
|
||||
async add(noteId: string) {
|
||||
if (!(await checkIsUserPremium(CHECK_IDS.vaultAdd))) return;
|
||||
|
||||
await this.lockNote({ id: noteId }, await this.getVaultPassword());
|
||||
await this.db.noteHistory.clearSessions(noteId);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import Database from "../api/index.js";
|
||||
import { sanitizeTag } from "./tags.js";
|
||||
import { SQLCollection } from "../database/sql-collection.js";
|
||||
import { isFalse } from "../database/index.js";
|
||||
import { CHECK_IDS, checkIsUserPremium } from "../common.js";
|
||||
|
||||
export const DefaultColors: Record<string, string> = {
|
||||
red: "#f44336",
|
||||
@@ -85,7 +84,7 @@ export class Colors implements ICollection {
|
||||
await this.collection.update([oldColor.id], item);
|
||||
return oldColor.id;
|
||||
}
|
||||
if (!(await checkIsUserPremium(CHECK_IDS.noteColor))) return;
|
||||
if (!this.db.features.allowed("colors")) return;
|
||||
|
||||
const id = item.id || getId(item.dateCreated);
|
||||
await this.collection.upsert({
|
||||
|
||||
@@ -25,11 +25,6 @@ import { SQLCollection } from "../database/sql-collection.js";
|
||||
import { DatabaseSchema, isFalse } from "../database/index.js";
|
||||
import { Kysely, sql, Transaction } from "@streetwriters/kysely";
|
||||
import { deleteItems } from "../utils/array.js";
|
||||
import {
|
||||
CHECK_IDS,
|
||||
checkIsUserPremium,
|
||||
FREE_NOTEBOOKS_LIMIT
|
||||
} from "../common.js";
|
||||
|
||||
export class Notebooks implements ICollection {
|
||||
name = "notebooks";
|
||||
@@ -64,12 +59,7 @@ export class Notebooks implements ICollection {
|
||||
if (oldNotebook && isTrashItem(oldNotebook))
|
||||
throw new Error("Cannot modify trashed notebooks.");
|
||||
|
||||
if (
|
||||
!oldNotebook &&
|
||||
(await this.all.count()) >= FREE_NOTEBOOKS_LIMIT &&
|
||||
!(await checkIsUserPremium(CHECK_IDS.notebookAdd))
|
||||
)
|
||||
return;
|
||||
if (!oldNotebook && !(await this.db.features.allowed("notebooks"))) return;
|
||||
|
||||
const mergedNotebook: Partial<Notebook> = {
|
||||
...oldNotebook,
|
||||
|
||||
@@ -99,6 +99,16 @@ export class Reminders implements ICollection {
|
||||
);
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this.collection.createFilter<Reminder>(
|
||||
(qb) =>
|
||||
qb
|
||||
.where(isFalse("deleted"))
|
||||
.where((eb) => eb.parens(createIsReminderActiveQuery())),
|
||||
this.db.options?.batchSize
|
||||
);
|
||||
}
|
||||
|
||||
exists(itemId: string) {
|
||||
return this.collection.exists(itemId);
|
||||
}
|
||||
@@ -276,7 +286,7 @@ export function createUpcomingReminderTimeQuery(unix = "now") {
|
||||
const lastSelectedDay = sql`(SELECT MAX(value) FROM json_each(selectedDays))`;
|
||||
|
||||
const monthDate = sql`strftime('%m-%d%H:%M', date / 1000, 'unixepoch', 'localtime')`;
|
||||
return sql`CASE
|
||||
return sql`CASE
|
||||
WHEN mode = 'once' THEN date / 1000
|
||||
WHEN recurringMode = 'year' THEN
|
||||
strftime('%s',
|
||||
@@ -286,11 +296,11 @@ export function createUpcomingReminderTimeQuery(unix = "now") {
|
||||
)
|
||||
WHEN recurringMode = 'day' THEN
|
||||
strftime('%s',
|
||||
${dateNow} || ${time},
|
||||
${dateNow} || ${time},
|
||||
IIF(${dateTime} <= ${dateTimeNow}, '+1 day', '+0 day'),
|
||||
'utc'
|
||||
)
|
||||
WHEN recurringMode = 'week' AND selectedDays IS NOT NULL AND json_array_length(selectedDays) > 0 THEN
|
||||
WHEN recurringMode = 'week' AND selectedDays IS NOT NULL AND json_array_length(selectedDays) > 0 THEN
|
||||
CASE
|
||||
WHEN ${weekDayNow} > ${lastSelectedDay}
|
||||
OR (${weekDayNow} == ${lastSelectedDay} AND ${dateTime} <= ${dateTimeNow})
|
||||
@@ -316,7 +326,7 @@ export function createIsReminderActiveQuery(now = "now") {
|
||||
return sql`IIF(
|
||||
(disabled IS NULL OR disabled = 0)
|
||||
AND (mode != 'once'
|
||||
OR datetime(date / 1000, 'unixepoch', 'localtime') > datetime(${now})
|
||||
OR datetime(date / 1000, 'unixepoch', 'localtime') > datetime(${now})
|
||||
OR (snoozeUntil IS NOT NULL
|
||||
AND datetime(snoozeUntil / 1000, 'unixepoch', 'localtime') > datetime(${now}))
|
||||
), 1, 0)`.$castTo<boolean>();
|
||||
|
||||
@@ -24,7 +24,6 @@ import { ICollection } from "./collection.js";
|
||||
import { SQLCollection } from "../database/sql-collection.js";
|
||||
import { isFalse } from "../database/index.js";
|
||||
import { sql } from "@streetwriters/kysely";
|
||||
import { CHECK_IDS, checkIsUserPremium } from "../common.js";
|
||||
|
||||
export class Tags implements ICollection {
|
||||
name = "tags";
|
||||
@@ -65,11 +64,7 @@ export class Tags implements ICollection {
|
||||
await this.collection.update([oldTag.id], item);
|
||||
return oldTag.id;
|
||||
}
|
||||
if (
|
||||
(await this.all.count()) >= 5 &&
|
||||
!(await checkIsUserPremium(CHECK_IDS.noteTag))
|
||||
)
|
||||
return;
|
||||
if (!(await this.db.features.allowed("tags"))) return;
|
||||
|
||||
const id = item.id || getId();
|
||||
await this.collection.upsert({
|
||||
|
||||
@@ -29,11 +29,6 @@ import {
|
||||
} from "../utils/grouping.js";
|
||||
import { sql } from "@streetwriters/kysely";
|
||||
import { MAX_SQL_PARAMETERS } from "../database/sql-collection.js";
|
||||
import {
|
||||
CHECK_IDS,
|
||||
checkIsUserPremium,
|
||||
FREE_NOTEBOOKS_LIMIT
|
||||
} from "../common.js";
|
||||
import { withSubNotebooks } from "./notebooks.js";
|
||||
|
||||
export default class Trash {
|
||||
@@ -202,13 +197,11 @@ export default class Trash {
|
||||
}
|
||||
|
||||
if (notebookIds.length > 0) {
|
||||
const notebooksLimitReached =
|
||||
(await this.db.notebooks.all.count()) + notebookIds.length >
|
||||
FREE_NOTEBOOKS_LIMIT;
|
||||
const isUserPremium = await checkIsUserPremium(CHECK_IDS.notebookAdd);
|
||||
if (notebooksLimitReached && !isUserPremium) {
|
||||
const expectedCount =
|
||||
(await this.db.notebooks.all.count()) + notebookIds.length;
|
||||
if (!(await this.db.features.allowed("notebooks", expectedCount)))
|
||||
return false;
|
||||
}
|
||||
|
||||
const ids = [...notebookIds, ...(await this.subNotebooks(notebookIds))];
|
||||
await this.db.notebooks.collection.update(ids, {
|
||||
type: "notebook",
|
||||
|
||||
@@ -21,17 +21,6 @@ import EventManager from "./utils/event-manager.js";
|
||||
|
||||
export const EV = new EventManager();
|
||||
|
||||
export async function checkIsUserPremium(type: string) {
|
||||
// if (process.env.NODE_ENV === "test") return true;
|
||||
|
||||
const results = await EV.publishWithResult<{ type: string; result: boolean }>(
|
||||
EVENTS.userCheckStatus,
|
||||
type
|
||||
);
|
||||
if (typeof results === "boolean") return results;
|
||||
return results.some((r) => r.type === type && r.result === true);
|
||||
}
|
||||
|
||||
export const SYNC_CHECK_IDS = {
|
||||
autoSync: "autoSync",
|
||||
sync: "sync"
|
||||
@@ -80,17 +69,7 @@ export function sendMigrationProgressEvent(
|
||||
|
||||
export const CLIENT_ID = "notesnook";
|
||||
|
||||
export const CHECK_IDS = {
|
||||
noteColor: "note:color",
|
||||
noteTag: "note:tag",
|
||||
noteExport: "note:export",
|
||||
vaultAdd: "vault:add",
|
||||
notebookAdd: "notebook:add",
|
||||
backupEncrypt: "backup:encrypt"
|
||||
};
|
||||
|
||||
export const EVENTS = {
|
||||
userCheckStatus: "user:checkStatus",
|
||||
userSubscriptionUpdated: "user:subscriptionUpdated",
|
||||
userEmailConfirmed: "user:emailConfirmed",
|
||||
userLoggedIn: "user:loggedIn",
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Paragraph } from "../paragraph/index.js";
|
||||
import { Heading } from "../heading/index.js";
|
||||
import { TextSelection } from "@tiptap/pm/state";
|
||||
import { Fragment } from "@tiptap/pm/model";
|
||||
import { hasPermission } from "../../types.js";
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
@@ -132,6 +133,8 @@ export const Callout = Node.create({
|
||||
setCallout:
|
||||
(attributes) =>
|
||||
({ tr, state }) => {
|
||||
if (!hasPermission("setCallout")) return false;
|
||||
|
||||
const { selection } = state;
|
||||
const start = selection.from;
|
||||
const end = selection.to;
|
||||
@@ -176,6 +179,7 @@ export const Callout = Node.create({
|
||||
new InputRule({
|
||||
find: CALLOUT_REGEX,
|
||||
handler: ({ state, range, match }) => {
|
||||
if (!hasPermission("setCallout", true)) return null;
|
||||
if (match.length === 1) return null;
|
||||
|
||||
const calloutType = (match[1] || "info") as CalloutType;
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { hasPermission } from "../../types.js";
|
||||
import { getParentAttributes } from "../../utils/prosemirror.js";
|
||||
import { Node, mergeAttributes, wrappingInputRule } from "@tiptap/core";
|
||||
|
||||
@@ -83,6 +84,8 @@ export const OutlineList = Node.create<OutlineListOptions>({
|
||||
toggleOutlineList:
|
||||
() =>
|
||||
({ chain }) => {
|
||||
if (!hasPermission("toggleOutlineList")) return false;
|
||||
|
||||
return chain()
|
||||
.toggleList(
|
||||
this.name,
|
||||
@@ -107,22 +110,35 @@ export const OutlineList = Node.create<OutlineListOptions>({
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
return [
|
||||
wrappingInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
keepMarks: this.options.keepMarks,
|
||||
keepAttributes: this.options.keepAttributes,
|
||||
editor: this.editor,
|
||||
getAttributes: () => {
|
||||
return getParentAttributes(
|
||||
this.editor,
|
||||
this.options.keepMarks,
|
||||
this.options.keepAttributes
|
||||
);
|
||||
}
|
||||
})
|
||||
];
|
||||
const inputRule = wrappingInputRule({
|
||||
find: inputRegex,
|
||||
type: this.type,
|
||||
keepMarks: this.options.keepMarks,
|
||||
keepAttributes: this.options.keepAttributes,
|
||||
editor: this.editor,
|
||||
getAttributes: () => {
|
||||
return getParentAttributes(
|
||||
this.editor,
|
||||
this.options.keepMarks,
|
||||
this.options.keepAttributes
|
||||
);
|
||||
}
|
||||
});
|
||||
const oldHandler = inputRule.handler;
|
||||
inputRule.handler = ({ state, range, match, chain, can, commands }) => {
|
||||
if (!hasPermission("toggleOutlineList", true)) return;
|
||||
|
||||
oldHandler({
|
||||
state,
|
||||
range,
|
||||
match,
|
||||
chain,
|
||||
can,
|
||||
commands
|
||||
});
|
||||
};
|
||||
|
||||
return [inputRule];
|
||||
},
|
||||
addNodeView() {
|
||||
return ({ node, HTMLAttributes }) => {
|
||||
|
||||
@@ -41,6 +41,7 @@ import { Node as ProsemirrorNode } from "@tiptap/pm/model";
|
||||
import { TaskItemNode } from "../task-item/index.js";
|
||||
import { ListItem } from "../list-item/list-item.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { hasPermission } from "../../types.js";
|
||||
|
||||
type TaskListStats = { checked: number; total: number };
|
||||
export type TaskListAttributes = {
|
||||
@@ -121,6 +122,8 @@ export const TaskListNode = TaskList.extend({
|
||||
toggleTaskList:
|
||||
() =>
|
||||
({ editor, chain, state, tr }) => {
|
||||
if (!hasPermission("toggleTaskList")) return false;
|
||||
|
||||
const { $from, $to } = state.selection;
|
||||
|
||||
chain()
|
||||
@@ -338,6 +341,8 @@ export const TaskListNode = TaskList.extend({
|
||||
});
|
||||
const oldHandler = inputRule.handler;
|
||||
inputRule.handler = ({ state, range, match, chain, can, commands }) => {
|
||||
if (!hasPermission("toggleTaskList", true)) return;
|
||||
|
||||
const $from = state.selection.$from;
|
||||
const parentNode = $from.node($from.depth - 1);
|
||||
if (parentNode.type.name === ListItem.name) {
|
||||
|
||||
@@ -21,14 +21,16 @@ import { UnionCommands } from "@tiptap/core";
|
||||
import { useEffect } from "react";
|
||||
import { PermissionRequestEvent } from "../types.js";
|
||||
|
||||
export type Claims = "premium";
|
||||
export type Claims = keyof typeof ClaimsMap;
|
||||
export type PermissionHandlerOptions = {
|
||||
claims: Record<Claims, boolean>;
|
||||
onPermissionDenied: (claim: Claims, id: keyof UnionCommands) => void;
|
||||
onPermissionDenied: (claim: Claims, silent: boolean) => void;
|
||||
};
|
||||
|
||||
const ClaimsMap: Record<Claims, (keyof UnionCommands)[]> = {
|
||||
premium: ["insertImage", "insertAttachment"]
|
||||
const ClaimsMap = {
|
||||
callout: ["setCallout"] as (keyof UnionCommands)[],
|
||||
outlineList: ["toggleOutlineList"] as (keyof UnionCommands)[],
|
||||
taskList: ["toggleTaskList"] as (keyof UnionCommands)[]
|
||||
};
|
||||
|
||||
export function usePermissionHandler(options: PermissionHandlerOptions) {
|
||||
@@ -37,7 +39,7 @@ export function usePermissionHandler(options: PermissionHandlerOptions) {
|
||||
useEffect(() => {
|
||||
function onPermissionRequested(ev: Event) {
|
||||
const {
|
||||
detail: { id }
|
||||
detail: { id, silent }
|
||||
} = ev as PermissionRequestEvent;
|
||||
|
||||
for (const key in ClaimsMap) {
|
||||
@@ -47,7 +49,7 @@ export function usePermissionHandler(options: PermissionHandlerOptions) {
|
||||
if (commands.indexOf(id) <= -1) continue;
|
||||
if (claims[claim]) continue;
|
||||
|
||||
onPermissionDenied(claim, id);
|
||||
onPermissionDenied(claim, silent);
|
||||
ev.preventDefault();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -322,12 +322,7 @@ const uploadImageFromURLMobile = (editor: Editor): MenuItem => ({
|
||||
component: ({ onClick }) => (
|
||||
<ImageUploadPopup
|
||||
onInsert={(image) => {
|
||||
editor
|
||||
.requestPermission("insertImage")
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertImage(image)
|
||||
.run();
|
||||
editor?.chain().focus().insertImage(image).run();
|
||||
onClick?.();
|
||||
}}
|
||||
onClose={() => {
|
||||
@@ -350,12 +345,7 @@ const uploadImageFromURL = (editor: Editor): MenuItem => ({
|
||||
popup: (hide) => (
|
||||
<ImageUploadPopup
|
||||
onInsert={(image) => {
|
||||
editor
|
||||
.requestPermission("insertImage")
|
||||
?.chain()
|
||||
.focus()
|
||||
.insertImage(image)
|
||||
.run();
|
||||
editor?.chain().focus().insertImage(image).run();
|
||||
hide();
|
||||
}}
|
||||
onClose={hide}
|
||||
|
||||
@@ -20,21 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { UnionCommands, Editor as TiptapEditor } from "@tiptap/core";
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
export type PermissionRequestEvent = CustomEvent<{ id: keyof UnionCommands }>;
|
||||
export type PermissionRequestEvent = CustomEvent<{
|
||||
id: keyof UnionCommands;
|
||||
silent: boolean;
|
||||
}>;
|
||||
|
||||
export class Editor extends TiptapEditor {
|
||||
private mutex: Mutex = new Mutex();
|
||||
|
||||
/**
|
||||
* Request permission before executing a command to make sure user
|
||||
* is allowed to perform the action.
|
||||
* @param id the command id to get permission for
|
||||
* @returns latest editor instance
|
||||
*/
|
||||
requestPermission(id: keyof UnionCommands): TiptapEditor | undefined {
|
||||
return hasPermission(id) ? this : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs editor state changes in a thread-safe manner using a mutex
|
||||
* ensuring that all changes are applied sequentially. Use this when
|
||||
@@ -45,9 +38,12 @@ export class Editor extends TiptapEditor {
|
||||
}
|
||||
}
|
||||
|
||||
export function hasPermission(id: keyof UnionCommands): boolean {
|
||||
export function hasPermission(
|
||||
id: keyof UnionCommands,
|
||||
silent = false
|
||||
): boolean {
|
||||
const event = new CustomEvent("permissionrequest", {
|
||||
detail: { id },
|
||||
detail: { id, silent },
|
||||
cancelable: true
|
||||
});
|
||||
return window.dispatchEvent(event);
|
||||
|
||||
@@ -2739,8 +2739,8 @@ msgid "File mismatch"
|
||||
msgstr "File mismatch"
|
||||
|
||||
#: src/strings.ts:944
|
||||
msgid "File size should be less than {sizeInMB}MB"
|
||||
msgstr "File size should be less than {sizeInMB}MB"
|
||||
msgid "File size should be less than {sizeInMB}"
|
||||
msgstr "File size should be less than {sizeInMB}"
|
||||
|
||||
#: src/strings.ts:942
|
||||
msgid "File too big"
|
||||
@@ -3153,8 +3153,8 @@ msgid "Image settings"
|
||||
msgstr "Image settings"
|
||||
|
||||
#: src/strings.ts:946
|
||||
msgid "Image size should be less than {sizeInMB}MB"
|
||||
msgstr "Image size should be less than {sizeInMB}MB"
|
||||
msgid "Image size should be less than {sizeInMB}"
|
||||
msgstr "Image size should be less than {sizeInMB}"
|
||||
|
||||
#: src/strings.ts:754
|
||||
msgid "Images"
|
||||
|
||||
@@ -2728,7 +2728,7 @@ msgid "File mismatch"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:944
|
||||
msgid "File size should be less than {sizeInMB}MB"
|
||||
msgid "File size should be less than {sizeInMB}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:942
|
||||
@@ -3133,7 +3133,7 @@ msgid "Image settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:946
|
||||
msgid "Image size should be less than {sizeInMB}MB"
|
||||
msgid "Image size should be less than {sizeInMB}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:754
|
||||
|
||||
@@ -940,10 +940,10 @@ $headline$: Use starting line of the note as title.`,
|
||||
encryptingAttachmentDesc: (name = "attachment") =>
|
||||
t`Please wait while we encrypt ${name} for upload.`,
|
||||
fileTooLarge: () => t`File too big`,
|
||||
fileTooLargeDesc: (sizeInMB: number) =>
|
||||
t`File size should be less than ${sizeInMB}MB`,
|
||||
imageTooLarge: (sizeInMB: number) =>
|
||||
t`Image size should be less than ${sizeInMB}MB`,
|
||||
fileTooLargeDesc: (sizeInMB: string) =>
|
||||
t`File size should be less than ${sizeInMB}`,
|
||||
imageTooLarge: (sizeInMB: string) =>
|
||||
t`Image size should be less than ${sizeInMB}`,
|
||||
failToOpen: () => t`Failed to open`,
|
||||
fileMismatch: () => t`File mismatch`,
|
||||
noNoteProperties: () => t`Start writing to create a new note`,
|
||||
|
||||
Reference in New Issue
Block a user