mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
84 Commits
docs/self-
...
3.3.5-andr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efec2dc898 | ||
|
|
5bd032e480 | ||
|
|
c1bd4cfb01 | ||
|
|
0b943d8ecd | ||
|
|
75e22a591e | ||
|
|
66498e12ce | ||
|
|
d5fefd2f10 | ||
|
|
e2556d3228 | ||
|
|
2eb38d236e | ||
|
|
d9c57162f1 | ||
|
|
9f8f0e8413 | ||
|
|
ea52893be1 | ||
|
|
eef63d2028 | ||
|
|
14e0112e17 | ||
|
|
8ec9034fb2 | ||
|
|
dc5973d75d | ||
|
|
12126b2f9f | ||
|
|
9fa256be0e | ||
|
|
c103328098 | ||
|
|
ea2022bbe4 | ||
|
|
d7ced57319 | ||
|
|
2cc200dc2b | ||
|
|
baa74d0353 | ||
|
|
72b906da14 | ||
|
|
6db8c2fac2 | ||
|
|
09d5c0a65e | ||
|
|
0b6b6a02cc | ||
|
|
f49c673e8e | ||
|
|
64a8e9a075 | ||
|
|
e998b07ca5 | ||
|
|
c189279030 | ||
|
|
12ddd6f958 | ||
|
|
da06afda8a | ||
|
|
bf808b7a2e | ||
|
|
13476a3f50 | ||
|
|
cfdf77a7b5 | ||
|
|
070c90362a | ||
|
|
c8d9858a0c | ||
|
|
2685f60409 | ||
|
|
8d3bb5185c | ||
|
|
1f748a4026 | ||
|
|
b27f064ad4 | ||
|
|
27f781cc63 | ||
|
|
1427958eca | ||
|
|
436e09c255 | ||
|
|
cc9b2dd5b0 | ||
|
|
0ba25419f3 | ||
|
|
9238de4cb4 | ||
|
|
2d5fe1058c | ||
|
|
b01d1fc87e | ||
|
|
9f4c0a4c9a | ||
|
|
9eebda86c2 | ||
|
|
d43e856fea | ||
|
|
7af1596038 | ||
|
|
652f6eab0b | ||
|
|
fdaddfdb92 | ||
|
|
9a1adf2065 | ||
|
|
f10b258f58 | ||
|
|
7f558cbd41 | ||
|
|
641eb28ce7 | ||
|
|
2c59a5f6f9 | ||
|
|
712f02e61e | ||
|
|
e5ad405a23 | ||
|
|
cd1b78d8df | ||
|
|
3b1ca46945 | ||
|
|
96c1636f88 | ||
|
|
a10f949856 | ||
|
|
9ff4757ed6 | ||
|
|
17c1e77f54 | ||
|
|
187a2a7262 | ||
|
|
4621230549 | ||
|
|
d7cb3317a2 | ||
|
|
6d81f26864 | ||
|
|
5219814b5a | ||
|
|
bef9fd0fd0 | ||
|
|
105fb0fc69 | ||
|
|
89c408981c | ||
|
|
8c621f9f47 | ||
|
|
74544efd62 | ||
|
|
c30ccbb68d | ||
|
|
12e3335d52 | ||
|
|
f29439539d | ||
|
|
13c9fdba52 | ||
|
|
95f52fba51 |
2
.github/workflows/desktop.publish.yml
vendored
2
.github/workflows/desktop.publish.yml
vendored
@@ -268,7 +268,6 @@ jobs:
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build snap
|
||||
if: inputs.publish-snap
|
||||
@@ -336,7 +335,6 @@ jobs:
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
# - name: Build snap
|
||||
# if: inputs.publish-snap
|
||||
|
||||
3
.github/workflows/desktop.tests.yml
vendored
3
.github/workflows/desktop.tests.yml
vendored
@@ -67,7 +67,6 @@ jobs:
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
@@ -111,7 +110,6 @@ jobs:
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
@@ -161,7 +159,6 @@ jobs:
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npm run tx @notesnook/desktop:release
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
|
||||
12
apps/desktop/package-lock.json
generated
12
apps/desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -30,7 +30,7 @@
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^36.0.0",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "0.21.5",
|
||||
"node-abi": "^4.5.0",
|
||||
@@ -3412,9 +3412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "36.9.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-36.9.3.tgz",
|
||||
"integrity": "sha512-eR5yswsA55zVTPDEIA/PSdVNBLOp0q0Wsavgx0S3BmJYOqKoH1gqzS+hggf0/aY5OvUjVNSHiJJA1VsB5aJUug==",
|
||||
"version": "37.6.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-37.6.1.tgz",
|
||||
"integrity": "sha512-aHtJVNjqf0lk7dlPoc1X+fMBpZtLn+XGvP6IYc3gooTwsD1D/Ic2SBRC9SnIk6LkWTsDaSF9jgH1d9Q7eABy/Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.2",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@types/node": "22.15.3",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.3",
|
||||
"electron": "^36.0.0",
|
||||
"electron": "^37.0.0",
|
||||
"electron-builder": "^26.0.12",
|
||||
"esbuild": "0.21.5",
|
||||
"node-abi": "^4.5.0",
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function setupDatabase(password?: string) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
// const base = `http://192.168.100.88`;
|
||||
// const base = `http://192.168.100.92`;
|
||||
|
||||
// database.host({
|
||||
// API_HOST: `${base}:5264`,
|
||||
|
||||
@@ -38,9 +38,6 @@ export function hideAuth(context) {
|
||||
context === "intro"
|
||||
) {
|
||||
Navigation.replace("FluidPanelsView");
|
||||
setTimeout(() => {
|
||||
Navigation.resetRootState();
|
||||
}, 1000);
|
||||
} else {
|
||||
Navigation.goBack();
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ const PDFPreview = () => {
|
||||
textAlign: "center",
|
||||
marginRight: 4,
|
||||
borderRadius: 3,
|
||||
fontFamily: "OpenSans-Regular"
|
||||
fontFamily: "Inter-Regular"
|
||||
}}
|
||||
selectTextOnFocus
|
||||
keyboardType="decimal-pad"
|
||||
|
||||
@@ -919,7 +919,6 @@ const ReviewItem = (props: {
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const PricingPlanCard = ({
|
||||
plan,
|
||||
pricingPlans,
|
||||
@@ -932,7 +931,6 @@ const PricingPlanCard = ({
|
||||
setStep: (step: number) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const isFreePlan = plan.id === "free";
|
||||
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
|
||||
const { width } = useWindowDimensions();
|
||||
const isTablet = width > 600;
|
||||
@@ -942,19 +940,22 @@ const PricingPlanCard = ({
|
||||
regionalDiscount?.sku ||
|
||||
`notesnook.${plan.id}.${annualBilling ? "yearly" : "monthly"}`
|
||||
];
|
||||
const price = pricingPlans?.getPrice(
|
||||
product as RNIap.Subscription,
|
||||
pricingPlans.hasTrialOffer(plan.id, product?.productId) ? 1 : 0,
|
||||
annualBilling
|
||||
);
|
||||
|
||||
const WebPlan = pricingPlans?.getWebPlan(
|
||||
plan.id,
|
||||
annualBilling ? "yearly" : "monthly"
|
||||
);
|
||||
|
||||
const price = pricingPlans?.getPrice(
|
||||
pricingPlans.isGithubRelease && WebPlan
|
||||
? WebPlan
|
||||
: (product as RNIap.Subscription),
|
||||
pricingPlans.hasTrialOffer(plan.id, product?.productId) ? 1 : 0,
|
||||
annualBilling
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pricingPlans?.isGithubRelease) return;
|
||||
if (pricingPlans?.isGithubRelease || !annualBilling) return;
|
||||
pricingPlans
|
||||
?.getRegionalDiscount(
|
||||
plan.id,
|
||||
@@ -965,7 +966,13 @@ const PricingPlanCard = ({
|
||||
.then((value) => {
|
||||
setRegionaDiscount(value);
|
||||
});
|
||||
}, []);
|
||||
}, [annualBilling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!annualBilling) {
|
||||
setRegionaDiscount(undefined);
|
||||
}
|
||||
}, [annualBilling]);
|
||||
|
||||
const isSubscribed =
|
||||
product?.productId &&
|
||||
@@ -1110,24 +1117,21 @@ const PricingPlanCard = ({
|
||||
) : (
|
||||
<View>
|
||||
<Paragraph size={AppFontSize.lg}>
|
||||
{isFreePlan
|
||||
? "0.00"
|
||||
: price ||
|
||||
`${WebPlan?.price?.currency} ${WebPlan?.price?.gross}`}{" "}
|
||||
{price || `${WebPlan?.price?.currency} ${WebPlan?.price?.gross}`}{" "}
|
||||
<Paragraph>/{strings.month()}</Paragraph>
|
||||
</Paragraph>
|
||||
|
||||
{isFreePlan || !product ? null : (
|
||||
{!product && !WebPlan ? null : (
|
||||
<Paragraph color={colors.secondary.paragraph} size={AppFontSize.xs}>
|
||||
{annualBilling
|
||||
? strings.billedAnnually(
|
||||
pricingPlans?.getStandardPrice(
|
||||
product as RNIap.Subscription
|
||||
(product || WebPlan) as any
|
||||
) as string
|
||||
)
|
||||
: strings.billedMonthly(
|
||||
pricingPlans?.getStandardPrice(
|
||||
product as RNIap.Subscription
|
||||
(product || WebPlan) as any
|
||||
) as string
|
||||
)}
|
||||
</Paragraph>
|
||||
|
||||
@@ -21,7 +21,13 @@ import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import Config from "react-native-config";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
@@ -60,27 +66,24 @@ export const BuyPlan = (props: {
|
||||
return checkoutUrl ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<WebView
|
||||
source={{
|
||||
uri: checkoutUrl
|
||||
<Paragraph>{strings.finishPurchaseInBrowser()}</Paragraph>
|
||||
<Button
|
||||
title={strings.next()}
|
||||
type="accent"
|
||||
onPress={() => {
|
||||
pricingPlans.finish();
|
||||
}}
|
||||
onMessage={(message) => {
|
||||
try {
|
||||
const data = JSON.parse(message.nativeEvent.data);
|
||||
if (data.success) {
|
||||
pricingPlans.finish();
|
||||
}
|
||||
} catch (e) {}
|
||||
}}
|
||||
domStorageEnabled
|
||||
javaScriptEnabled
|
||||
cacheEnabled
|
||||
enabled
|
||||
style={{
|
||||
flex: 1
|
||||
/>
|
||||
<Button
|
||||
title={strings.goBack()}
|
||||
onPress={() => {
|
||||
setCheckoutUrl(undefined);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
@@ -245,12 +248,14 @@ export const BuyPlan = (props: {
|
||||
}
|
||||
onPress={async () => {
|
||||
if (isGithubRelease) {
|
||||
setCheckoutUrl(
|
||||
await db.subscriptions.checkoutUrl(
|
||||
(pricingPlans.selectedProduct as Plan).plan,
|
||||
(pricingPlans.selectedProduct as Plan).period
|
||||
)
|
||||
const url = await db.subscriptions.checkoutUrl(
|
||||
(pricingPlans.selectedProduct as Plan).plan,
|
||||
(pricingPlans.selectedProduct as Plan).period
|
||||
);
|
||||
if (url) {
|
||||
setCheckoutUrl(url);
|
||||
Linking.openURL(url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import Config from "react-native-config";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -67,7 +68,8 @@ App version: ${getVersion()}
|
||||
Platform: ${Platform.OS}
|
||||
Device: ${getBrand() || ""}-${getModel() || ""}-${getSystemVersion() || ""}
|
||||
Pro: ${PremiumService.get()}
|
||||
Logged in: ${user ? "yes" : "no"}`,
|
||||
Logged in: ${user ? "yes" : "no"}
|
||||
Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
|
||||
userId: user?.id
|
||||
});
|
||||
if (!issueUrl.current) {
|
||||
@@ -159,7 +161,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
fontFamily: "Inter-Regular",
|
||||
marginBottom: DefaultAppStyles.GAP_VERTICAL,
|
||||
fontSize: AppFontSize.md,
|
||||
color: colors.primary.heading
|
||||
@@ -192,7 +194,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
fontFamily: "Inter-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: AppFontSize.sm,
|
||||
marginBottom: 2.5,
|
||||
|
||||
@@ -239,7 +239,7 @@ const Input = ({
|
||||
flexShrink: 1,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL - 4,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL - 4,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
fontFamily: "Inter-Regular",
|
||||
...(inputStyle as ViewStyle)
|
||||
};
|
||||
|
||||
|
||||
@@ -132,7 +132,10 @@ const SheetWrapper = ({
|
||||
width: 100,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
safeAreaInsets={insets}
|
||||
safeAreaInsets={{
|
||||
...insets,
|
||||
bottom: Platform.OS === "android" ? 0 : insets.bottom
|
||||
}}
|
||||
statusBarTranslucent
|
||||
drawUnderStatusBar={true}
|
||||
containerStyle={style}
|
||||
|
||||
@@ -270,11 +270,13 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
if (Platform.OS === "android") {
|
||||
if (isGithubRelease) {
|
||||
if (!(product as Plan)?.price) return null;
|
||||
return `${(product as Plan).price.currency} ${
|
||||
return `${
|
||||
(product as Plan).currencySymbol || (product as Plan).currency
|
||||
} ${
|
||||
(product as Plan).period === "yearly"
|
||||
? ((product as Plan).price.gross / 12).toFixed(2)
|
||||
? (product as Plan).price.gross
|
||||
: (product as Plan).period === "5-year"
|
||||
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
|
||||
? (product as Plan).price.gross
|
||||
: (product as Plan).price.gross
|
||||
}`;
|
||||
}
|
||||
@@ -599,7 +601,9 @@ const usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
|
||||
if (isGithubRelease) {
|
||||
if (!(product as Plan)?.price) return null;
|
||||
return `${(product as Plan).price.currency} ${
|
||||
return `${
|
||||
(product as Plan).currencySymbol || (product as Plan).currency
|
||||
} ${
|
||||
(product as Plan).period === "yearly"
|
||||
? ((product as Plan).price.gross / 12).toFixed(2)
|
||||
: (product as Plan).period === "5-year"
|
||||
|
||||
@@ -258,12 +258,14 @@ export const RootNavigation = () => {
|
||||
(state) => state.settings.introCompleted
|
||||
);
|
||||
const clearSelection = useSelectionStore((state) => state.clearSelection);
|
||||
const resetTimer = React.useRef<NodeJS.Timeout>(undefined);
|
||||
const onStateChange = React.useCallback(
|
||||
(state: any) => {
|
||||
if (useSelectionStore.getState().selectionMode) {
|
||||
clearSelection();
|
||||
}
|
||||
setTimeout(() => {
|
||||
clearTimeout(resetTimer.current);
|
||||
resetTimer.current = setTimeout(() => {
|
||||
Navigation.resetRootState(state);
|
||||
}, 1000);
|
||||
hideAllTooltips();
|
||||
|
||||
@@ -44,7 +44,8 @@
|
||||
"@trpc/react-query": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"async-mutex": "0.5.0"
|
||||
"async-mutex": "0.5.0",
|
||||
"react-async-hook": "^4.0.0"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export const SearchBar = ({
|
||||
testID="search-input"
|
||||
style={{
|
||||
fontSize: AppFontSize.sm,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
fontFamily: "Inter-Regular",
|
||||
flexGrow: 1,
|
||||
color: colors.primary.paragraph,
|
||||
paddingTop: 0,
|
||||
|
||||
@@ -43,6 +43,7 @@ import { ServersConfiguration } from "./server-config";
|
||||
import SoundPicker from "./sound-picker";
|
||||
import ThemeSelector from "./theme-selector";
|
||||
import { TitleFormat } from "./title-format";
|
||||
import { NotesnookCircle } from "./notesnook-circle";
|
||||
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
homeselector: <HomePicker />,
|
||||
@@ -69,5 +70,6 @@ export const components: { [name: string]: ReactElement } = {
|
||||
),
|
||||
"sidebar-tab-selector": <SidebarTabPicker />,
|
||||
"change-password": <ChangePassword />,
|
||||
"change-email": <ChangeEmail />
|
||||
"change-email": <ChangeEmail />,
|
||||
"notesnook-circle": <NotesnookCircle />
|
||||
};
|
||||
|
||||
190
apps/mobile/app/screens/settings/notesnook-circle.tsx
Normal file
190
apps/mobile/app/screens/settings/notesnook-circle.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { CirclePartner, SubscriptionStatus } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import AppIcon from "../../components/ui/AppIcon";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
|
||||
export const NotesnookCircle = () => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const isOnTrial =
|
||||
PremiumService.get() &&
|
||||
user?.subscription?.status === SubscriptionStatus.TRIAL;
|
||||
const isFree = !PremiumService.get();
|
||||
const partners = useAsync(db.circle.partners, []);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
{!isFree && !isOnTrial ? null : (
|
||||
<View>
|
||||
<Paragraph>
|
||||
{isFree
|
||||
? strings.freeUserCircleNotice()
|
||||
: strings.trialUserCircleNotice()}
|
||||
</Paragraph>
|
||||
|
||||
{!isOnTrial ? null : (
|
||||
<Button
|
||||
title={strings.upgradePlan()}
|
||||
onPress={() => {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: true,
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out"
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{partners.loading ? <ActivityIndicator /> : null}
|
||||
|
||||
{partners.error ? (
|
||||
<Notice type="alert" text={partners.error.message} />
|
||||
) : null}
|
||||
|
||||
{partners.result?.map((item) => (
|
||||
<Partner item={item} available={!isFree && !isOnTrial} />
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const Partner = ({
|
||||
item,
|
||||
available
|
||||
}: {
|
||||
item: CirclePartner;
|
||||
available: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [code, setCode] = useState<string>();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading>{item.name}</Heading>
|
||||
<Image
|
||||
src={item.logoBase64}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph style={{ textAlign: "justify" }}>
|
||||
{item.longDescription.trim()}
|
||||
</Paragraph>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{item.offerDescription}
|
||||
</Paragraph>
|
||||
|
||||
{available ? (
|
||||
<>
|
||||
{!code ? (
|
||||
<Button
|
||||
type="accent"
|
||||
title={strings.redeemCode()}
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
if (!PremiumService.get()) {
|
||||
Navigation.navigate("PayWall", {
|
||||
canGoBack: true,
|
||||
context: useUserStore.getState().user
|
||||
? "logged-in"
|
||||
: "logged-out"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
db.circle
|
||||
.redeem(item.id)
|
||||
.then((result) => setCode(result?.code))
|
||||
.catch((e) => ToastManager.error(e));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: DefaultAppStyles.GAP_SMALL,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.secondary.border,
|
||||
flexDirection: "row",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
Clipboard.setString(code);
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.lg}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{code}
|
||||
</Paragraph>
|
||||
|
||||
<AppIcon name="content-copy" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -615,6 +615,14 @@ export const settingsGroups: SettingSection[] = [
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "notesnook-circle",
|
||||
name: strings.notesnookCircle(),
|
||||
icon: "circle-outline",
|
||||
type: "screen",
|
||||
description: strings.notesnookCircleDesc(),
|
||||
component: "notesnook-circle"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ async function setPremiumStatus() {
|
||||
|
||||
async function loadProductsAndSubs() {
|
||||
try {
|
||||
if (Config.GITHUB_RELEASE === "true") throw new Error("Github release");
|
||||
if (!subs || subs.length === 0) {
|
||||
subs = await RNIap.getSubscriptions({
|
||||
skus: itemSkus
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { notesnook } from "../test.ids";
|
||||
import { TestBuilder } from "./utils";
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3071
|
||||
versionCode 3074
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="com.streetwriters.notesnook" />
|
||||
<data android:scheme="notesnook" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
- We have launches new pricing plans, Essential, Pro and Believer
|
||||
- Free users can now upload attachments and create private vault
|
||||
- Collapsible Headings
|
||||
- Notesnook Circle
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -56,9 +56,10 @@
|
||||
<string>group.org.streetwriters.notesnook</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>OpenSans-Bold.ttf</string>
|
||||
<string>OpenSans-SemiBold.ttf</string>
|
||||
<string>OpenSans-Regular.ttf</string>
|
||||
<string>Inter-Bold.ttf</string>
|
||||
<string>Inter-SemiBold.ttf</string>
|
||||
<string>Inter-Regular.ttf</string>
|
||||
<string>Inter-Italic.ttf</string>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,14 +17,15 @@
|
||||
2E4BF736C2EB268ED3FD9C29 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B5216B66C0B4A91C0A69128B /* PrivacyInfo.xcprivacy */; };
|
||||
6510E6D72877215700DACAA9 /* build.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6510E6D62877215700DACAA9 /* build.bundle */; };
|
||||
6515C42F2580AA3000E83E39 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6515C42E2580AA2F00E83E39 /* StoreKit.framework */; };
|
||||
6517B7C12B6838EB0079FF37 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */; };
|
||||
6517B7C22B6838EB0079FF37 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */; };
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */; };
|
||||
656835812BB29A9800144BAB /* OpenSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 656835802BB29A8300144BAB /* OpenSans-Italic.ttf */; };
|
||||
6569927F2C5754F10041CD41 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
|
||||
656992802C5754F10041CD41 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */; };
|
||||
656992812C5754F10041CD41 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */; };
|
||||
653F4E082EA3887C009A42A2 /* Inter-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E042EA3887C009A42A2 /* Inter-Bold.ttf */; };
|
||||
653F4E092EA3887C009A42A2 /* Inter-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E052EA3887C009A42A2 /* Inter-Italic.ttf */; };
|
||||
653F4E0A2EA3887C009A42A2 /* Inter-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E072EA3887C009A42A2 /* Inter-SemiBold.ttf */; };
|
||||
653F4E0B2EA3887C009A42A2 /* Inter-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E062EA3887C009A42A2 /* Inter-Regular.ttf */; };
|
||||
653F4E0C2EA3887C009A42A2 /* Inter-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E042EA3887C009A42A2 /* Inter-Bold.ttf */; };
|
||||
653F4E0D2EA3887C009A42A2 /* Inter-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E052EA3887C009A42A2 /* Inter-Italic.ttf */; };
|
||||
653F4E0E2EA3887C009A42A2 /* Inter-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E072EA3887C009A42A2 /* Inter-SemiBold.ttf */; };
|
||||
653F4E0F2EA3887C009A42A2 /* Inter-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 653F4E062EA3887C009A42A2 /* Inter-Regular.ttf */; };
|
||||
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6593E4A2281C345400492C50 /* AppDelegate.mm */; };
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */ = {isa = PBXBuildFile; fileRef = 659BE46625E11A5100E05671 /* notesnook-text.png */; };
|
||||
65AA857925E6DDEC00772A01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857825E6DDEC00772A01 /* WidgetKit.framework */; };
|
||||
@@ -117,12 +118,12 @@
|
||||
4C4AF691C324D95337F2FB0A /* libPods-Make Note.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Make Note.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6510E6D62877215700DACAA9 /* build.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; name = build.bundle; path = "../../../../packages/editor-mobile/build.bundle"; sourceTree = "<group>"; };
|
||||
6515C42E2580AA2F00E83E39 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Regular.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Regular.ttf"; sourceTree = "<group>"; };
|
||||
6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-SemiBold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-SemiBold.ttf"; sourceTree = "<group>"; };
|
||||
6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Bold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Bold.ttf"; sourceTree = "<group>"; };
|
||||
6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = BootSplash.storyboard; path = Notesnook/BootSplash.storyboard; sourceTree = "<group>"; };
|
||||
6529D2B0257B4A2900B49BC3 /* NotesnookDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = NotesnookDebug.entitlements; path = Notesnook/NotesnookDebug.entitlements; sourceTree = "<group>"; };
|
||||
656835802BB29A8300144BAB /* OpenSans-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "OpenSans-Italic.ttf"; path = "../../../../packages/editor-mobile/public/fonts/OpenSans-Italic.ttf"; sourceTree = "<group>"; };
|
||||
653F4E042EA3887C009A42A2 /* Inter-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Inter-Bold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/Inter-Bold.ttf"; sourceTree = SOURCE_ROOT; };
|
||||
653F4E052EA3887C009A42A2 /* Inter-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Inter-Italic.ttf"; path = "../../../../packages/editor-mobile/public/fonts/Inter-Italic.ttf"; sourceTree = SOURCE_ROOT; };
|
||||
653F4E062EA3887C009A42A2 /* Inter-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Inter-Regular.ttf"; path = "../../../../packages/editor-mobile/public/fonts/Inter-Regular.ttf"; sourceTree = SOURCE_ROOT; };
|
||||
653F4E072EA3887C009A42A2 /* Inter-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Inter-SemiBold.ttf"; path = "../../../../packages/editor-mobile/public/fonts/Inter-SemiBold.ttf"; sourceTree = SOURCE_ROOT; };
|
||||
6593E4A2281C345400492C50 /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = Notesnook/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
659BE46625E11A5100E05671 /* notesnook-text.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "notesnook-text.png"; path = "Notesnook/Images.xcassets/notesnook-text.png"; sourceTree = "<group>"; };
|
||||
65A7F34F255689AD00699170 /* Notesnook.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Notesnook.entitlements; path = Notesnook/Notesnook.entitlements; sourceTree = "<group>"; };
|
||||
@@ -303,10 +304,10 @@
|
||||
83CBB9F61A601CBA00E9B192 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
656835802BB29A8300144BAB /* OpenSans-Italic.ttf */,
|
||||
6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */,
|
||||
6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */,
|
||||
6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */,
|
||||
653F4E042EA3887C009A42A2 /* Inter-Bold.ttf */,
|
||||
653F4E052EA3887C009A42A2 /* Inter-Italic.ttf */,
|
||||
653F4E062EA3887C009A42A2 /* Inter-Regular.ttf */,
|
||||
653F4E072EA3887C009A42A2 /* Inter-SemiBold.ttf */,
|
||||
65C400DD2A80B68D00AA3DF5 /* MaterialCommunityIcons.ttf */,
|
||||
65C149862A61151B005C40F1 /* extension.bundle */,
|
||||
6510E6D62877215700DACAA9 /* build.bundle */,
|
||||
@@ -555,15 +556,15 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
656835812BB29A9800144BAB /* OpenSans-Italic.ttf in Resources */,
|
||||
65C400DE2A80B68D00AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */,
|
||||
6517B7C22B6838EB0079FF37 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */,
|
||||
6510E6D72877215700DACAA9 /* build.bundle in Resources */,
|
||||
6517B7C12B6838EB0079FF37 /* OpenSans-Regular.ttf in Resources */,
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */,
|
||||
653F4E0C2EA3887C009A42A2 /* Inter-Bold.ttf in Resources */,
|
||||
653F4E0D2EA3887C009A42A2 /* Inter-Italic.ttf in Resources */,
|
||||
653F4E0E2EA3887C009A42A2 /* Inter-SemiBold.ttf in Resources */,
|
||||
653F4E0F2EA3887C009A42A2 /* Inter-Regular.ttf in Resources */,
|
||||
E7F9D56665C2F19F4A97BF49 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -596,10 +597,11 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6569927F2C5754F10041CD41 /* OpenSans-Bold.ttf in Resources */,
|
||||
656992802C5754F10041CD41 /* OpenSans-Regular.ttf in Resources */,
|
||||
656992812C5754F10041CD41 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
65C400DF2A80B6B600AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
653F4E082EA3887C009A42A2 /* Inter-Bold.ttf in Resources */,
|
||||
653F4E092EA3887C009A42A2 /* Inter-Italic.ttf in Resources */,
|
||||
653F4E0A2EA3887C009A42A2 /* Inter-SemiBold.ttf in Resources */,
|
||||
653F4E0B2EA3887C009A42A2 /* Inter-Regular.ttf in Resources */,
|
||||
65C149872A61151B005C40F1 /* extension.bundle in Resources */,
|
||||
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */,
|
||||
);
|
||||
@@ -1089,7 +1091,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2150;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1163,7 +1165,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.2;
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1194,7 +1196,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2150;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1268,7 +1270,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.2;
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1427,7 +1429,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2150;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1439,7 +1441,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.2;
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1470,7 +1472,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2150;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1483,7 +1485,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.2;
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1513,7 +1515,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2150;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1587,14 +1589,14 @@
|
||||
"\"$(PODS_ROOT)/Headers/Private/React-Core\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Make Note/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.3.2;
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1625,7 +1627,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2150;
|
||||
CURRENT_PROJECT_VERSION = 2153;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1700,14 +1702,14 @@
|
||||
"\"$(PODS_ROOT)/Headers/Private/React-Core\"",
|
||||
);
|
||||
INFOPLIST_FILE = "Make Note/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
|
||||
MARKETING_VERSION = 3.3.2;
|
||||
MARKETING_VERSION = 3.3.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1784,10 +1786,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
@@ -1852,10 +1851,7 @@
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_CFLAGS = "$(inherited)";
|
||||
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
" ",
|
||||
);
|
||||
OTHER_LDFLAGS = "$(inherited) ";
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
USE_HERMES = true;
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "1"
|
||||
BundleIdentifier = "com.apple.mobilesafari"
|
||||
RemotePath = "/Applications/MobileSafari.app">
|
||||
RemotePath = "/Library/Developer/CoreSimulator/Volumes/iOS_22G86/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 18.6.simruntime/Contents/Resources/RuntimeRoot/Applications/MobileSafari.app">
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
@@ -92,6 +92,7 @@
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
askForAppToLaunch = "Yes"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
@@ -89,9 +88,10 @@
|
||||
<string>group.org.streetwriters.notesnook</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>OpenSans-Italic.ttf</string>
|
||||
<string>OpenSans-SemiBold.ttf</string>
|
||||
<string>OpenSans-Regular.ttf</string>
|
||||
<string>Inter-Italic.ttf</string>
|
||||
<string>Inter-SemiBold.ttf</string>
|
||||
<string>Inter-Regular.ttf</string>
|
||||
<string>Inter-Bold.ttf</string>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
|
||||
342
apps/mobile/package-lock.json
generated
342
apps/mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.2",
|
||||
"version": "3.3.5",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -70,6 +70,7 @@ const EXTRA_ICON_NAMES = [
|
||||
"chevron-down",
|
||||
"calendar",
|
||||
"minus-circle-outline",
|
||||
"circle-outline",
|
||||
"close-circle-outline",
|
||||
"qrcode",
|
||||
"text",
|
||||
|
||||
@@ -19,7 +19,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import ShareExtension from "@ammarahmed/react-native-share-extension";
|
||||
import { getPreviewData } from "@flyerhq/react-native-link-preview";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import {
|
||||
formatBytes,
|
||||
isFeatureAvailable,
|
||||
useIsFeatureAvailable
|
||||
} from "@notesnook/common";
|
||||
import { isImage } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
@@ -119,6 +123,7 @@ const ShareView = () => {
|
||||
const noteTitle = useRef("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingExtension, setLoadingExtension] = useState(true);
|
||||
const fullQualityImages = useIsFeatureAvailable("fullQualityImages");
|
||||
const [rawData, setRawData] = useState({
|
||||
type: null,
|
||||
value: null
|
||||
@@ -161,6 +166,12 @@ const ShareView = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fullQualityImages?.isAllowed) {
|
||||
setCompress(true);
|
||||
}
|
||||
}, [fullQualityImages]);
|
||||
|
||||
const showLinkPreview = async (note, link) => {
|
||||
let _note = note;
|
||||
_note.content.data = makeHtmlFromUrl(link);
|
||||
@@ -223,14 +234,14 @@ const ShareView = () => {
|
||||
}
|
||||
} else {
|
||||
const user = await db.user.getUser();
|
||||
if (user && user.subscription.type !== 0) {
|
||||
if (
|
||||
(isImage(item.type) && item.size > IMAGE_SIZE_LIMIT) ||
|
||||
(!isImage(item.type) && item.size > FILE_SIZE_LIMIT)
|
||||
) {
|
||||
if (user) {
|
||||
const feature = await isFeatureAvailable(
|
||||
"fileSize",
|
||||
item.size || 0
|
||||
);
|
||||
if (!feature.isAllowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setRawFiles((files) => {
|
||||
const index = files.findIndex(
|
||||
(file) => file.name === item.name
|
||||
@@ -491,7 +502,7 @@ const ShareView = () => {
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontFamily: "Inter-SemiBold",
|
||||
fontSize: AppFontSize.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
@@ -506,7 +517,7 @@ const ShareView = () => {
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "OpenSans-SemiBold",
|
||||
fontFamily: "Inter-SemiBold",
|
||||
fontSize: AppFontSize.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
@@ -632,7 +643,9 @@ const ShareView = () => {
|
||||
marginTop: 6
|
||||
}}
|
||||
onPress={() => {
|
||||
setCompress(!compress);
|
||||
if (fullQualityImages?.isAllowed) {
|
||||
setCompress(!compress);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -643,7 +656,7 @@ const ShareView = () => {
|
||||
: "checkbox-blank-outline"
|
||||
}
|
||||
color={
|
||||
compress
|
||||
compress && fullQualityImages?.isAllowed
|
||||
? colors.primary.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
|
||||
1
apps/monograph/package-lock.json
generated
1
apps/monograph/package-lock.json
generated
@@ -208,6 +208,7 @@
|
||||
"happy-dom": "16.0.1",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"prismjs": "^1.30.0",
|
||||
"prosemirror-test-builder": "^1.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
2
apps/theme-builder/package-lock.json
generated
2
apps/theme-builder/package-lock.json
generated
@@ -987,7 +987,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.3.0-beta.1",
|
||||
"version": "3.3.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
|
||||
1898
apps/web/package-lock.json
generated
1898
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.2",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@lingui/react": "5.1.2",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/react": "1.6.1",
|
||||
"@notesnook-importer/core": "^2.2.2",
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook/common": "file:../../packages/common",
|
||||
"@notesnook/core": "file:../../packages/core",
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
|
||||
@@ -23,11 +23,7 @@ import { useStore as useUserStore } from "./stores/user-store";
|
||||
import { useEditorStore } from "./stores/editor-store";
|
||||
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { useStore as useSettingStore } from "./stores/setting-store";
|
||||
import {
|
||||
resetNotices,
|
||||
scheduleBackups,
|
||||
scheduleFullBackups
|
||||
} from "./common/notices";
|
||||
import { scheduleBackups, scheduleFullBackups } from "./common/notices";
|
||||
import { introduceFeatures, resetFeatures } from "./common";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { db } from "./common/db";
|
||||
@@ -63,7 +59,7 @@ export default function AppEffects() {
|
||||
await refreshNavItems();
|
||||
await updateLastSynced();
|
||||
await initUser();
|
||||
await resetNotices();
|
||||
// await resetNotices();
|
||||
setIsVaultCreated(await db.vault.exists());
|
||||
|
||||
await FeatureDialog.show({ featureName: "highlights" });
|
||||
|
||||
@@ -53,8 +53,8 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
MONOGRAPH_HOST: "https://monogr.ph",
|
||||
NOTESNOOK_HOST: "https://notesnook.com",
|
||||
...Config.get("serverUrls", {})
|
||||
});
|
||||
|
||||
@@ -89,9 +89,6 @@ export async function scheduleFullBackups() {
|
||||
export async function shouldAddAutoBackupsDisabledNotice() {
|
||||
if (isIgnored("autoBackupsOff")) return false;
|
||||
|
||||
const user = await db.user.getUser();
|
||||
if (!user) return false;
|
||||
|
||||
const backupInterval = Config.get("backupReminderOffset", 0);
|
||||
return backupInterval === 0;
|
||||
}
|
||||
@@ -201,9 +198,11 @@ function isIgnored(key: keyof typeof NoticesData) {
|
||||
|
||||
let openedToast: { hide: () => void } | null = null;
|
||||
async function saveBackup(mode: "full" | "partial" = "partial") {
|
||||
if (IS_TESTING) return;
|
||||
|
||||
if (IS_DESKTOP_APP) {
|
||||
await createBackup({ noVerify: true, mode, background: true });
|
||||
} else if (!IS_TESTING) {
|
||||
} else {
|
||||
if (openedToast !== null) return;
|
||||
openedToast = showToast(
|
||||
"success",
|
||||
|
||||
@@ -704,8 +704,10 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
||||
);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
editor.style.marginLeft = `-${negativeSpace}px`;
|
||||
editor.style.marginRight = `-${negativeSpace}px`;
|
||||
if (!isMobile() && !isTablet()) {
|
||||
editor.style.marginLeft = `-${negativeSpace}px`;
|
||||
editor.style.marginRight = `-${negativeSpace}px`;
|
||||
}
|
||||
editor.style.paddingLeft = `${negativeSpace}px`;
|
||||
editor.style.paddingRight = `${negativeSpace}px`;
|
||||
});
|
||||
|
||||
@@ -19,8 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {
|
||||
INetworkProvider,
|
||||
OneNote,
|
||||
OneNoteSettings,
|
||||
ProviderSettings,
|
||||
transform
|
||||
} from "@notesnook-importer/core";
|
||||
@@ -46,17 +44,7 @@ function getProviderSettings(
|
||||
provider: INetworkProvider<ProviderSettings>,
|
||||
settings: ProviderSettings
|
||||
) {
|
||||
if (provider instanceof OneNote) {
|
||||
return {
|
||||
...settings,
|
||||
cache: false,
|
||||
clientId: "6c32bdbd-c6c6-4cda-bcf0-0c8ec17e5804",
|
||||
redirectUri:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: "https://app.notesnook.com"
|
||||
} as OneNoteSettings;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function NetworkProviderHandler(props: NetworkProviderHandlerProps) {
|
||||
|
||||
@@ -50,7 +50,6 @@ const TRIAL_PERIODS: Record<Period, number> = {
|
||||
};
|
||||
|
||||
export function toPricingInfo(plan: Plan, user: User | undefined): PricingInfo {
|
||||
console.log("TO PRICING INFO", plan);
|
||||
return {
|
||||
country: plan.country,
|
||||
period: plan.period,
|
||||
@@ -58,9 +57,9 @@ export function toPricingInfo(plan: Plan, user: User | undefined): PricingInfo {
|
||||
currency: plan.currency,
|
||||
id: plan.id,
|
||||
period: plan.period,
|
||||
subtotal: formatPrice(plan.price.gross, plan.currency),
|
||||
subtotal: formatPrice(plan.price.net, plan.currency),
|
||||
tax: formatPrice(plan.price.tax, plan.currency),
|
||||
total: formatPrice(plan.price.net, plan.currency),
|
||||
total: formatPrice(plan.price.gross, plan.currency),
|
||||
trial_period: isTrialAvailableForPlan(plan.plan, user)
|
||||
? {
|
||||
frequency: TRIAL_PERIODS[plan.period]
|
||||
@@ -74,9 +73,9 @@ export function toPricingInfo(plan: Plan, user: User | undefined): PricingInfo {
|
||||
currency: plan.currency,
|
||||
id: plan.id,
|
||||
period: plan.period,
|
||||
subtotal: formatPrice(plan.price.gross, plan.currency),
|
||||
subtotal: formatPrice(plan.price.net, plan.currency),
|
||||
tax: formatPrice(plan.price.tax, plan.currency),
|
||||
total: formatPrice(plan.price.net, plan.currency)
|
||||
total: formatPrice(plan.price.gross, plan.currency)
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
||||
@@ -245,7 +245,7 @@ function getPricingInfo(
|
||||
id: price.price_id,
|
||||
period: options.period,
|
||||
subtotal: formatPrice(
|
||||
data.recurring_totals.subtotal,
|
||||
data.recurring_totals.subtotal - data.recurring_totals.discount,
|
||||
data.currency_code
|
||||
),
|
||||
total: formatPrice(data.recurring_totals.total, data.currency_code),
|
||||
@@ -256,7 +256,10 @@ function getPricingInfo(
|
||||
currency: data.currency_code,
|
||||
id: price.price_id,
|
||||
period: options.period,
|
||||
subtotal: formatPrice(totals.subtotal, data.currency_code),
|
||||
subtotal: formatPrice(
|
||||
totals.subtotal - totals.discount,
|
||||
data.currency_code
|
||||
),
|
||||
total: formatPrice(totals.total, data.currency_code),
|
||||
tax: formatPrice(totals.tax, data.currency_code),
|
||||
trial_period: price.trial_period
|
||||
|
||||
@@ -93,14 +93,14 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "New pricing plans",
|
||||
title: "Notesnook Circle",
|
||||
subtitle:
|
||||
"We are changing our pricing to make more sense and be more flexible."
|
||||
"Notesnook Circle brings together trusted partners who share our commitment to privacy, transparency, and user freedom. As a member you get exclusive discounts and offers from our partners. Check it out in Settings > Notesnook Circle."
|
||||
},
|
||||
{
|
||||
title: "Filters in search",
|
||||
title: "Collapsible headings",
|
||||
subtitle:
|
||||
"Search now has some basic filters like content:, title:, tag:, color: etc. For the whole list of filters, read our docs."
|
||||
"You can now collapse and expand headings in your notes. This makes it easier to manage large notes and focus on specific sections."
|
||||
}
|
||||
],
|
||||
cta: {
|
||||
|
||||
173
apps/web/src/dialogs/settings/components/circle-partners.tsx
Normal file
173
apps/web/src/dialogs/settings/components/circle-partners.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Copy, Loading } from "../../../components/icons";
|
||||
import { Button, Link, Flex, Text, Grid } from "@theme-ui/components";
|
||||
import { usePromise } from "@notesnook/common";
|
||||
import { db } from "../../../common/db";
|
||||
import { writeToClipboard } from "../../../utils/clipboard";
|
||||
import { showToast } from "../../../utils/toast";
|
||||
import { ErrorText } from "../../../components/error-text";
|
||||
import { useStore as useUserStore } from "../../../stores/user-store";
|
||||
import { SubscriptionPlan, SubscriptionStatus } from "@notesnook/core";
|
||||
import { BuyDialog } from "../../buy-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export function CirclePartners() {
|
||||
const partners = usePromise(() => db.circle.partners(), []);
|
||||
const [redeemedCode, setRedeemedCode] = useState<{
|
||||
partnerId: string;
|
||||
code: string;
|
||||
}>();
|
||||
const subscription = useUserStore((store) => store.user?.subscription);
|
||||
|
||||
const isFree =
|
||||
!subscription ||
|
||||
subscription?.plan === SubscriptionPlan.FREE ||
|
||||
subscription?.status === SubscriptionStatus.EXPIRED;
|
||||
const isTrial = subscription?.status === SubscriptionStatus.TRIAL;
|
||||
const canRedeem = !isFree && !isTrial;
|
||||
|
||||
return (
|
||||
<>
|
||||
{partners.status === "pending" ? (
|
||||
<Loading sx={{ mt: 2 }} />
|
||||
) : partners.status === "rejected" ? (
|
||||
<ErrorText error={partners.reason} />
|
||||
) : (
|
||||
<Grid columns={2} sx={{ mt: 2 }}>
|
||||
{partners.value?.map((partner) => (
|
||||
<Flex
|
||||
key={partner.id}
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "dialog",
|
||||
padding: 2,
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{ justifyContent: "space-between", alignItems: "center" }}
|
||||
>
|
||||
<Text variant="title">{partner.name}</Text>
|
||||
<img
|
||||
src={partner.logoBase64}
|
||||
alt={partner.name}
|
||||
style={{ width: 30, height: "auto", borderRadius: 8 }}
|
||||
/>
|
||||
</Flex>
|
||||
<Text variant="body" sx={{ whiteSpace: "pre-wrap" }}>
|
||||
{partner.shortDescription}
|
||||
</Text>
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{
|
||||
fontStyle: "italic",
|
||||
color: "accent",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{partner.offerDescription}
|
||||
</Text>
|
||||
{redeemedCode?.partnerId === partner.id ? (
|
||||
<>
|
||||
<Flex
|
||||
sx={{
|
||||
bg: "background-secondary",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "default",
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
p: "small"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
className="selectable"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: 14
|
||||
}}
|
||||
>
|
||||
{redeemedCode.code}
|
||||
</Text>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={() =>
|
||||
writeToClipboard({ "text/plain": redeemedCode.code })
|
||||
}
|
||||
>
|
||||
<Copy size={12} />
|
||||
</Button>
|
||||
</Flex>
|
||||
{partner.codeRedeemUrl ? (
|
||||
<Text variant="subBody" sx={{ textAlign: "center" }}>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={partner.codeRedeemUrl.replace(
|
||||
"{{code}}",
|
||||
redeemedCode.code
|
||||
)}
|
||||
>
|
||||
Click here
|
||||
</Link>{" "}
|
||||
to directly claim the promotion.
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ mt: 1 }}
|
||||
onClick={async () => {
|
||||
if (isFree) return BuyDialog.show({});
|
||||
if (isTrial) {
|
||||
return showToast(
|
||||
"error",
|
||||
strings.trialUserCircleNotice()
|
||||
);
|
||||
}
|
||||
if (!canRedeem) return;
|
||||
const result = await db.circle
|
||||
.redeem(partner.id)
|
||||
.catch((e) => {
|
||||
showToast("error", e.message);
|
||||
});
|
||||
if (result?.code) {
|
||||
setRedeemedCode({
|
||||
partnerId: partner.id,
|
||||
code: result.code
|
||||
});
|
||||
showToast("success", "Code redeemed successfully");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isFree ? strings.upgradeToRedeem() : strings.redeemCode()}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -39,7 +39,8 @@ import {
|
||||
Servers,
|
||||
ShieldLock,
|
||||
Sync,
|
||||
Inbox
|
||||
Inbox,
|
||||
CircleEmpty
|
||||
} from "../../components/icons";
|
||||
import NavigationItem from "../../components/navigation-menu/navigation-item";
|
||||
import { FlexScrollContainer } from "../../components/scroll-container";
|
||||
@@ -80,6 +81,7 @@ import { strings } from "@notesnook/intl";
|
||||
import { mdToHtml } from "../../utils/md";
|
||||
import { InboxSettings } from "./inbox-settings";
|
||||
import { withFeatureCheck } from "../../common";
|
||||
import { NotesnookCircleSettings } from "./notesnook-circle-settings";
|
||||
|
||||
type SettingsDialogProps = BaseDialogProps<false> & {
|
||||
activeSection?: SectionKeys;
|
||||
@@ -109,6 +111,12 @@ const sectionGroups: SectionGroup[] = [
|
||||
icon: Sync,
|
||||
isHidden: () => !useUserStore.getState().isLoggedIn
|
||||
},
|
||||
{
|
||||
key: "circle",
|
||||
title: "Notesnook Circle",
|
||||
icon: CircleEmpty,
|
||||
isHidden: () => !useUserStore.getState().isLoggedIn
|
||||
},
|
||||
{
|
||||
key: "inbox",
|
||||
title: "Inbox",
|
||||
@@ -185,7 +193,8 @@ const SettingsGroups = [
|
||||
...AboutSettings,
|
||||
...SubscriptionSettings,
|
||||
...ServersSettings,
|
||||
...InboxSettings
|
||||
...InboxSettings,
|
||||
...NotesnookCircleSettings
|
||||
];
|
||||
|
||||
// Thoughts:
|
||||
|
||||
43
apps/web/src/dialogs/settings/notesnook-circle-settings.ts
Normal file
43
apps/web/src/dialogs/settings/notesnook-circle-settings.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { CirclePartners } from "./components/circle-partners";
|
||||
import { SettingsGroup } from "./types";
|
||||
|
||||
export const NotesnookCircleSettings: SettingsGroup[] = [
|
||||
{
|
||||
header: strings.notesnookCircle(),
|
||||
key: "notesnook-circle",
|
||||
section: "circle",
|
||||
settings: [
|
||||
{
|
||||
key: "partners",
|
||||
title: "",
|
||||
description: strings.notesnookCircleDesc(),
|
||||
components: [
|
||||
{
|
||||
type: "custom",
|
||||
component: CirclePartners
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -41,7 +41,8 @@ export type SectionKeys =
|
||||
| "legal"
|
||||
| "developer"
|
||||
| "about"
|
||||
| "inbox";
|
||||
| "inbox"
|
||||
| "circle";
|
||||
|
||||
export type SectionGroupKeys =
|
||||
| "account"
|
||||
|
||||
@@ -25,7 +25,12 @@ import {
|
||||
IKVStore
|
||||
} from "./key-value";
|
||||
import { NNCrypto } from "./nncrypto";
|
||||
import type { Cipher, SerializedKey } from "@notesnook/crypto";
|
||||
import type {
|
||||
AsymmetricCipher,
|
||||
Cipher,
|
||||
SerializedKey,
|
||||
SerializedKeyPair
|
||||
} from "@notesnook/crypto";
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { IKeyStore } from "./key-store";
|
||||
import { User } from "@notesnook/core";
|
||||
@@ -160,6 +165,14 @@ export class NNStorage implements IStorage {
|
||||
return NNCrypto.decryptMulti(key, items, "text");
|
||||
}
|
||||
|
||||
decryptAsymmetric(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<"base64">
|
||||
): Promise<string> {
|
||||
cipherData.format = "base64";
|
||||
return NNCrypto.decryptAsymmetric(keyPair, cipherData, "base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -29,7 +29,7 @@ import { store as reminderStore } from "./reminder-store";
|
||||
import { store as settingStore } from "./setting-store";
|
||||
import BaseStore from "./index";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { Notice, resetNotices } from "../common/notices";
|
||||
import { Notice } from "../common/notices";
|
||||
import { EV, EVENTS, SYNC_CHECK_IDS, SyncOptions } from "@notesnook/core";
|
||||
import { logger } from "../utils/logger";
|
||||
import Config from "../utils/config";
|
||||
@@ -173,7 +173,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
logger.measure("refreshing app");
|
||||
|
||||
await this.updateLastSynced();
|
||||
await resetNotices();
|
||||
// await resetNotices();
|
||||
await noteStore.refresh();
|
||||
await notebookStore.refresh();
|
||||
await reminderStore.refresh();
|
||||
|
||||
@@ -38,26 +38,33 @@ export class ExportStream extends TransformStream<
|
||||
return;
|
||||
}
|
||||
if (item.type === "attachment") {
|
||||
report({ text: `Downloading attachment: ${item.path}` });
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile("exports", item.data.hash, item.data.chunkSize);
|
||||
const key = await db.attachments.decryptKey(item.data.key);
|
||||
if (!key) return;
|
||||
const stream = await streamingDecryptFile(item.data.hash, {
|
||||
key,
|
||||
iv: item.data.iv,
|
||||
name: item.data.filename,
|
||||
type: item.data.mimeType,
|
||||
isUploaded: !!item.data.dateUploaded
|
||||
});
|
||||
try {
|
||||
report({ text: `Downloading attachment: ${item.path}` });
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile("exports", item.data.hash, item.data.chunkSize);
|
||||
const key = await db.attachments.decryptKey(item.data.key);
|
||||
if (!key) return;
|
||||
const stream = await streamingDecryptFile(item.data.hash, {
|
||||
key,
|
||||
iv: item.data.iv,
|
||||
name: item.data.filename,
|
||||
type: item.data.mimeType,
|
||||
isUploaded: !!item.data.dateUploaded
|
||||
});
|
||||
|
||||
if (!stream) return;
|
||||
controller.enqueue({ ...item, data: stream });
|
||||
report({
|
||||
current: this.progress++,
|
||||
text: `Saving attachment: ${item.path}`
|
||||
});
|
||||
if (!stream) return;
|
||||
controller.enqueue({ ...item, data: stream });
|
||||
report({
|
||||
current: this.progress++,
|
||||
text: `Saving attachment: ${item.path}`
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
e.message = `Failed to export attachment: ${item.path}. ${e.message}`;
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
controller.enqueue(item);
|
||||
report({
|
||||
|
||||
@@ -119,7 +119,7 @@ function Checkout() {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentStep === 2) {
|
||||
var event = EV.subscribe(EVENTS.userSubscriptionUpdated, () => {
|
||||
const event = EV.subscribe(EVENTS.userSubscriptionUpdated, () => {
|
||||
hardNavigate("/notes#/welcome");
|
||||
});
|
||||
return () => {
|
||||
|
||||
@@ -47,7 +47,12 @@ function Payments() {
|
||||
if (_ptxn) {
|
||||
paddle.Checkout.open({
|
||||
transactionId: _ptxn,
|
||||
settings: { displayMode: "overlay" }
|
||||
settings: {
|
||||
displayMode: "overlay",
|
||||
allowDiscountRemoval: true,
|
||||
showAddDiscounts: true,
|
||||
showAddTaxId: true
|
||||
}
|
||||
});
|
||||
} else if (priceId) {
|
||||
paddle.Checkout.open({
|
||||
|
||||
@@ -9,70 +9,71 @@ The following keyboard shortcuts will help you navigate Notesnook faster.
|
||||
|
||||
### General
|
||||
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |
|
||||
| Search in notes list view if editor is not focused | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Settings | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Keyboard shortcuts | Ctrl / | Ctrl / | ⌘ / |
|
||||
| New note | - | Ctrl N | ⌘ N |
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| -------------------------------------------------- | ------ | ------------- | --- |
|
||||
| Search in notes list view if editor is not focused | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Settings | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Keyboard shortcuts | Ctrl / | Ctrl / | ⌘ / |
|
||||
| New note | - | Ctrl N | ⌘ N |
|
||||
|
||||
### Navigation
|
||||
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |
|
||||
| Next tab | Ctrl Alt → / Ctrl Alt ⇧ → | Ctrl tab | ⌘ tab |
|
||||
| Previous tab | Ctrl Alt ← / Ctrl Alt ⇧ ← | Ctrl ⇧ tab | ⌘ ⇧ tab |
|
||||
| Command palette | Ctrl ⇧ P | Ctrl ⇧ P | ⌘ ⇧ P |
|
||||
| Quick open | Ctrl P | Ctrl P | ⌘ P |
|
||||
| New tab | - | Ctrl T | ⌘ T |
|
||||
| Close active tab | - | Ctrl W | ⌘ W |
|
||||
| Close all tabs | - | Ctrl ⇧ W | ⌘ ⇧ W |
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| ---------------- | ------------------------- | ------------- | ------- |
|
||||
| Next tab | Ctrl Alt → / Ctrl Alt ⇧ → | Ctrl tab | ⌘ tab |
|
||||
| Previous tab | Ctrl Alt ← / Ctrl Alt ⇧ ← | Ctrl ⇧ tab | ⌘ ⇧ tab |
|
||||
| Command palette | Ctrl ⇧ P | Ctrl ⇧ P | ⌘ ⇧ P |
|
||||
| Quick open | Ctrl P | Ctrl P | ⌘ P |
|
||||
| New tab | - | Ctrl T | ⌘ T |
|
||||
| Close active tab | - | Ctrl W | ⌘ W |
|
||||
| Close all tabs | - | Ctrl ⇧ W | ⌘ ⇧ W |
|
||||
|
||||
### Editor
|
||||
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| --- | --- | --- | --- |
|
||||
| Add attachment | Ctrl ⇧ A | Ctrl ⇧ A | ⌘ ⇧ A |
|
||||
| Insert blockquote | Ctrl ⇧ B | Ctrl ⇧ B | ⌘ ⇧ B |
|
||||
| Toggle bold | Ctrl B | Ctrl B | ⌘ B |
|
||||
| Toggle bullet list | Ctrl ⇧ 8 | Ctrl ⇧ 8 | ⌘ ⇧ 8 |
|
||||
| Toggle check list | Ctrl ⇧ 9 | Ctrl ⇧ 9 | ⌘ ⇧ 9 |
|
||||
| Split list item | ↵ | ↵ | ↵ |
|
||||
| Lift list item | ⇧ Tab | ⇧ Tab | ⇧ Tab |
|
||||
| Sink list item | Ctrl ⇧ Down | Ctrl ⇧ Down | ⌘ ⇧ Down |
|
||||
| Toggle code | Ctrl E | Ctrl E | ⌘ E |
|
||||
| Toggle code block | Ctrl ⇧ C | Ctrl ⇧ C | ⌘ ⇧ C |
|
||||
| Insert date | Alt D | Alt D | ⌥ D |
|
||||
| Insert time | Alt T | Alt T | ⌥ T |
|
||||
| Insert date and time | Ctrl Alt D | Ctrl Alt D | ⌘ ⌥ D |
|
||||
| Insert date and time with timezone | Ctrl Alt Z | Ctrl Alt Z | ⌘ ⌥ Z |
|
||||
| Increase font size | Ctrl [ | Ctrl [ | ⌘ [ |
|
||||
| Decrease font size | Ctrl ] | Ctrl ] | ⌘ ] |
|
||||
| Insert paragraph | Ctrl ⇧ 0 | Ctrl ⇧ 0 | ⌘ ⇧ 0 |
|
||||
| Insert heading 1 | Ctrl Alt 1 | Ctrl Alt 1 | ⌘ ⌥ 1 |
|
||||
| Insert heading 2 | Ctrl Alt 2 | Ctrl Alt 2 | ⌘ ⌥ 2 |
|
||||
| Insert heading 3 | Ctrl Alt 3 | Ctrl Alt 3 | ⌘ ⌥ 3 |
|
||||
| Insert heading 4 | Ctrl Alt 4 | Ctrl Alt 4 | ⌘ ⌥ 4 |
|
||||
| Insert heading 5 | Ctrl Alt 5 | Ctrl Alt 5 | ⌘ ⌥ 5 |
|
||||
| Insert heading 6 | Ctrl Alt 6 | Ctrl Alt 6 | ⌘ ⌥ 6 |
|
||||
| Undo | Ctrl Z | Ctrl Z | ⌘ Z |
|
||||
| Redo | Ctrl ⇧ Z / Ctrl Y | Ctrl ⇧ Z / Ctrl Y | ⌘ ⇧ Z / ⌘ Y |
|
||||
| Add image | Ctrl ⇧ I | Ctrl ⇧ I | ⌘ ⇧ I |
|
||||
| Toggle italic | Ctrl I | Ctrl I | ⌘ I |
|
||||
| Remove formatting in selection | Ctrl \ | Ctrl \ | ⌘ \ |
|
||||
| Insert internal link | Ctrl ⇧ K | Ctrl ⇧ K | ⌘ ⇧ K |
|
||||
| Insert link | Ctrl K | Ctrl K | ⌘ K |
|
||||
| Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M |
|
||||
| Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 |
|
||||
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
|
||||
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
|
||||
| Open search | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
|
||||
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
|
||||
| Toggle task list | Ctrl ⇧ T | Ctrl ⇧ T | ⌘ ⇧ T |
|
||||
| Text align center | Ctrl ⇧ E | Ctrl ⇧ E | ⌘ ⇧ E |
|
||||
| Text align justify | Ctrl ⇧ J | Ctrl ⇧ J | ⌘ ⇧ J |
|
||||
| Text align left | Ctrl ⇧ L | Ctrl ⇧ L | ⌘ ⇧ L |
|
||||
| Text align right | Ctrl ⇧ R | Ctrl ⇧ R | ⌘ ⇧ R |
|
||||
| Underline | Ctrl U | Ctrl U | ⌘ U |
|
||||
| Description | Web | Windows/Linux | Mac |
|
||||
| ---------------------------------- | ----------------- | ----------------- | ----------- |
|
||||
| Add attachment | Ctrl ⇧ A | Ctrl ⇧ A | ⌘ ⇧ A |
|
||||
| Insert blockquote | Ctrl ⇧ B | Ctrl ⇧ B | ⌘ ⇧ B |
|
||||
| Toggle bold | Ctrl B | Ctrl B | ⌘ B |
|
||||
| Toggle bullet list | Ctrl ⇧ 8 | Ctrl ⇧ 8 | ⌘ ⇧ 8 |
|
||||
| Toggle check list | Ctrl ⇧ 9 | Ctrl ⇧ 9 | ⌘ ⇧ 9 |
|
||||
| Split list item | ↵ | ↵ | ↵ |
|
||||
| Lift list item | ⇧ Tab | ⇧ Tab | ⇧ Tab |
|
||||
| Sink list item | Ctrl ⇧ Down | Ctrl ⇧ Down | ⌘ ⇧ Down |
|
||||
| Toggle code | Ctrl E | Ctrl E | ⌘ E |
|
||||
| Toggle code block | Ctrl ⇧ C | Ctrl ⇧ C | ⌘ ⇧ C |
|
||||
| Insert date | Alt D | Alt D | ⌥ D |
|
||||
| Insert time | Alt T | Alt T | ⌥ T |
|
||||
| Insert date and time | Ctrl Alt D | Ctrl Alt D | ⌘ ⌥ D |
|
||||
| Insert date and time with timezone | Ctrl Alt Z | Ctrl Alt Z | ⌘ ⌥ Z |
|
||||
| Increase font size | Ctrl [ | Ctrl [ | ⌘ [ |
|
||||
| Decrease font size | Ctrl ] | Ctrl ] | ⌘ ] |
|
||||
| Insert paragraph | Ctrl ⇧ 0 | Ctrl ⇧ 0 | ⌘ ⇧ 0 |
|
||||
| Insert heading 1 | Ctrl Alt 1 | Ctrl Alt 1 | ⌘ ⌥ 1 |
|
||||
| Insert heading 2 | Ctrl Alt 2 | Ctrl Alt 2 | ⌘ ⌥ 2 |
|
||||
| Insert heading 3 | Ctrl Alt 3 | Ctrl Alt 3 | ⌘ ⌥ 3 |
|
||||
| Insert heading 4 | Ctrl Alt 4 | Ctrl Alt 4 | ⌘ ⌥ 4 |
|
||||
| Insert heading 5 | Ctrl Alt 5 | Ctrl Alt 5 | ⌘ ⌥ 5 |
|
||||
| Insert heading 6 | Ctrl Alt 6 | Ctrl Alt 6 | ⌘ ⌥ 6 |
|
||||
| Undo | Ctrl Z | Ctrl Z | ⌘ Z |
|
||||
| Redo | Ctrl ⇧ Z / Ctrl Y | Ctrl ⇧ Z / Ctrl Y | ⌘ ⇧ Z / ⌘ Y |
|
||||
| Add image | Ctrl ⇧ I | Ctrl ⇧ I | ⌘ ⇧ I |
|
||||
| Toggle italic | Ctrl I | Ctrl I | ⌘ I |
|
||||
| Remove formatting in selection | Ctrl \ | Ctrl \ | ⌘ \ |
|
||||
| Insert internal link | Ctrl ⇧ K | Ctrl ⇧ K | ⌘ ⇧ K |
|
||||
| Insert link | Ctrl K | Ctrl K | ⌘ K |
|
||||
| Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M |
|
||||
| Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 |
|
||||
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
|
||||
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
|
||||
| Open search | Ctrl F | Ctrl F | ⌘ F |
|
||||
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
|
||||
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
|
||||
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
|
||||
| Toggle task list | Ctrl ⇧ T | Ctrl ⇧ T | ⌘ ⇧ T |
|
||||
| Text align center | Ctrl ⇧ E | Ctrl ⇧ E | ⌘ ⇧ E |
|
||||
| Text align justify | Ctrl ⇧ J | Ctrl ⇧ J | ⌘ ⇧ J |
|
||||
| Text align left | Ctrl ⇧ L | Ctrl ⇧ L | ⌘ ⇧ L |
|
||||
| Text align right | Ctrl ⇧ R | Ctrl ⇧ R | ⌘ ⇧ R |
|
||||
| Underline | Ctrl U | Ctrl U | ⌘ U |
|
||||
| Toggle highlight | Ctrl Alt H | Ctrl Alt H | ⌘ ⌥ H |
|
||||
|
||||
5
extensions/web-clipper/package-lock.json
generated
5
extensions/web-clipper/package-lock.json
generated
@@ -86,9 +86,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.48.2",
|
||||
"slugify": "1.6.6",
|
||||
"webpack": "5.88.2",
|
||||
"webpack-cli": "5.1.4"
|
||||
"@rsbuild/core": "^1.5.13",
|
||||
"slugify": "1.6.6"
|
||||
}
|
||||
},
|
||||
"../../packages/theme": {
|
||||
|
||||
5
fastlane/metadata/android/en-US/changelogs/15359.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/15359.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- We have launches new pricing plans, Essential, Pro and Believer
|
||||
- Free users can now upload attachments and create private vault
|
||||
- Minor bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -447,6 +447,17 @@ const features = {
|
||||
believer: createLimit(true),
|
||||
legacyPro: createLimit(true)
|
||||
}
|
||||
}),
|
||||
notesnookCircle: createFeature({
|
||||
id: "notesnookCircle",
|
||||
title: "Notesnook Circle",
|
||||
availability: {
|
||||
free: createLimit(false),
|
||||
essential: createLimit(true),
|
||||
pro: createLimit(true),
|
||||
believer: createLimit(true),
|
||||
legacyPro: createLimit(true)
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -388,6 +388,12 @@ export const tiptapKeys = {
|
||||
description: "Underline",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
},
|
||||
toggleHighlight: {
|
||||
keys: "Mod-Alt-h",
|
||||
description: "Toggle highlight",
|
||||
category: "Editor",
|
||||
type: "tiptap"
|
||||
}
|
||||
} satisfies Record<string, TipTapKey>;
|
||||
|
||||
|
||||
48
packages/core/src/api/circle.ts
Normal file
48
packages/core/src/api/circle.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import hosts from "../utils/constants.js";
|
||||
import http from "../utils/http.js";
|
||||
import Database from "./index.js";
|
||||
|
||||
export type CirclePartner = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
logoBase64: string;
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
offerDescription: string;
|
||||
codeRedeemUrl?: string;
|
||||
};
|
||||
export class Circle {
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
partners(): Promise<CirclePartner[] | undefined> {
|
||||
return http.get(`${hosts.SUBSCRIPTIONS_HOST}/circle/partners`);
|
||||
}
|
||||
|
||||
async redeem(partnerId: string): Promise<{ code?: string } | undefined> {
|
||||
const token = await this.db.tokenManager.getAccessToken();
|
||||
return http.get(
|
||||
`${hosts.SUBSCRIPTIONS_HOST}/circle/redeem?partnerId=${partnerId}`,
|
||||
token
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ import { NNMigrationProvider } from "../database/migrations.js";
|
||||
import { ConfigStorage } from "../database/config.js";
|
||||
import { LazyPromise } from "../utils/lazy-promise.js";
|
||||
import { InboxApiKeys } from "./inbox-api-keys.js";
|
||||
import { Circle } from "./circle.js";
|
||||
|
||||
type EventSourceConstructor = new (
|
||||
uri: string,
|
||||
@@ -192,6 +193,7 @@ class Database {
|
||||
tokenManager = new TokenManager(this.kv);
|
||||
mfa = new MFAManager(this.tokenManager);
|
||||
subscriptions = new Subscriptions(this);
|
||||
circle = new Circle(this);
|
||||
offers = Offers;
|
||||
debug = new Debug();
|
||||
pricing = Pricing;
|
||||
|
||||
@@ -29,7 +29,7 @@ import Constants from "../../utils/constants.js";
|
||||
import TokenManager from "../token-manager.js";
|
||||
import Collector from "./collector.js";
|
||||
import { type HubConnection } from "@microsoft/signalr";
|
||||
import Merger from "./merger.js";
|
||||
import Merger, { handleInboxItems } from "./merger.js";
|
||||
import { AutoSync } from "./auto-sync.js";
|
||||
import { logger } from "../../logger.js";
|
||||
import { Mutex } from "async-mutex";
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
import {
|
||||
SYNC_COLLECTIONS_MAP,
|
||||
SyncableItemType,
|
||||
SyncInboxItem,
|
||||
SyncTransferItem
|
||||
} from "./types.js";
|
||||
import { DownloadableFile } from "../../database/fs.js";
|
||||
@@ -233,7 +234,19 @@ class Sync {
|
||||
async fetch(deviceId: string, options: SyncOptions) {
|
||||
await this.checkConnection();
|
||||
|
||||
await this.connection?.invoke("RequestFetchV2", deviceId);
|
||||
try {
|
||||
await this.connection?.invoke("RequestFetchV3", deviceId);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("HubException: Method does not exist")
|
||||
) {
|
||||
this.logger.warn(
|
||||
"RequestFetchV3 failed, falling back to RequestFetchV2"
|
||||
);
|
||||
await this.connection?.invoke("RequestFetchV2", deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.conflictedNoteIds.length > 0) {
|
||||
await this.db
|
||||
@@ -478,6 +491,19 @@ class Sync {
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.connection.on(
|
||||
"SendInboxItems",
|
||||
async (inboxItems: SyncInboxItem[]) => {
|
||||
if (this.connection?.state !== HubConnectionState.Connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await handleInboxItems(inboxItems, this.db);
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async getKey() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
MaybeDeletedItem,
|
||||
isDeleted
|
||||
} from "../../types.js";
|
||||
import { ParsedInboxItem, SyncInboxItem } from "./types.js";
|
||||
|
||||
const THRESHOLD = process.env.NODE_ENV === "test" ? 6 * 1000 : 60 * 1000;
|
||||
class Merger {
|
||||
@@ -146,3 +147,81 @@ export function isContentConflicted(
|
||||
return "merge";
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleInboxItems(
|
||||
inboxItems: SyncInboxItem[],
|
||||
db: Database
|
||||
) {
|
||||
const inboxKeys = await db.user.getInboxKeys();
|
||||
if (!inboxKeys) {
|
||||
logger.error("No inbox keys found, cannot process inbox items.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of inboxItems) {
|
||||
try {
|
||||
if (await db.notes.exists(item.id)) {
|
||||
logger.info("Inbox item already exists, skipping.", {
|
||||
inboxItemId: item.id
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const decryptedKey = await db.storage().decryptAsymmetric(inboxKeys, {
|
||||
alg: item.key.alg,
|
||||
cipher: item.key.cipher,
|
||||
format: "base64",
|
||||
length: item.key.length
|
||||
});
|
||||
const decryptedItem = await db.storage().decrypt(
|
||||
{ key: decryptedKey },
|
||||
{
|
||||
alg: item.alg,
|
||||
iv: item.iv,
|
||||
cipher: item.cipher,
|
||||
format: "base64",
|
||||
length: item.length,
|
||||
salt: item.salt
|
||||
}
|
||||
);
|
||||
const parsed = JSON.parse(decryptedItem) as ParsedInboxItem;
|
||||
if (parsed.type !== "note") {
|
||||
continue;
|
||||
}
|
||||
if (parsed.version !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await db.notes.add({
|
||||
id: item.id,
|
||||
title: parsed.title,
|
||||
favorite: parsed.favorite,
|
||||
pinned: parsed.pinned,
|
||||
readonly: parsed.readonly,
|
||||
content: {
|
||||
data: parsed?.content?.data ?? "",
|
||||
type: "tiptap"
|
||||
}
|
||||
});
|
||||
if (parsed.archived !== undefined) {
|
||||
await db.notes.archive(parsed.archived, item.id);
|
||||
}
|
||||
for (const notebookId of parsed.notebookIds || []) {
|
||||
if (!(await db.notebooks.exists(notebookId))) continue;
|
||||
await db.notes.addToNotebook(notebookId, item.id);
|
||||
}
|
||||
for (const tagId of parsed.tagIds || []) {
|
||||
if (!(await db.tags.exists(tagId))) continue;
|
||||
await db.relations.add(
|
||||
{ type: "tag", id: tagId },
|
||||
{ type: "note", id: item.id }
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to process inbox item.", {
|
||||
inboxItem: item
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,3 +48,24 @@ export type SyncTransferItem = {
|
||||
items: SyncItem[];
|
||||
type: SyncableItemType;
|
||||
};
|
||||
|
||||
export type SyncInboxItem = Omit<SyncItem, "format"> & {
|
||||
key: Omit<Cipher<"base64">, "format" | "salt" | "iv">;
|
||||
};
|
||||
|
||||
export type ParsedInboxItem = {
|
||||
title: string;
|
||||
pinned?: boolean;
|
||||
favorite?: boolean;
|
||||
readonly?: boolean;
|
||||
archived?: boolean;
|
||||
notebookIds?: string[];
|
||||
tagIds?: string[];
|
||||
type: "note";
|
||||
source: string;
|
||||
version: 1;
|
||||
content?: {
|
||||
type: "html";
|
||||
data: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ export * from "./api/debug.js";
|
||||
export * from "./api/monographs.js";
|
||||
export * from "./api/subscriptions.js";
|
||||
export * from "./api/pricing.js";
|
||||
export * from "./api/circle.js";
|
||||
export { VAULT_ERRORS } from "./api/vault.js";
|
||||
export type { SyncOptions } from "./api/sync/index.js";
|
||||
export { sanitizeTag } from "./collections/tags.js";
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
AsymmetricCipher,
|
||||
Cipher,
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
@@ -62,6 +63,10 @@ export interface IStorage {
|
||||
key: SerializedKey,
|
||||
items: Cipher<"base64">[]
|
||||
): Promise<string[]>;
|
||||
decryptAsymmetric(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<"base64">
|
||||
): Promise<string>;
|
||||
deriveCryptoKey(credentials: SerializedKey): Promise<void>;
|
||||
hash(
|
||||
password: string,
|
||||
|
||||
@@ -19,12 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { base64_variants, ISodium } from "@notesnook/sodium";
|
||||
import KeyUtils from "./keyutils.js";
|
||||
import { Cipher, Output, DataFormat, SerializedKey } from "./types.js";
|
||||
import {
|
||||
Cipher,
|
||||
Output,
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
SerializedKeyPair,
|
||||
AsymmetricCipher
|
||||
} from "./types.js";
|
||||
|
||||
export default class Decryption {
|
||||
private static transformInput(
|
||||
sodium: ISodium,
|
||||
cipherData: Cipher<DataFormat>
|
||||
cipherData: Cipher<DataFormat> | AsymmetricCipher<DataFormat>
|
||||
): Uint8Array {
|
||||
let input: Uint8Array | null = null;
|
||||
if (
|
||||
@@ -55,7 +62,6 @@ export default class Decryption {
|
||||
): Output<TOutputFormat> {
|
||||
if (!key.salt && cipherData.salt) key.salt = cipherData.salt;
|
||||
const encryptionKey = KeyUtils.transform(sodium, key);
|
||||
|
||||
const input = this.transformInput(sodium, cipherData);
|
||||
const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
||||
null,
|
||||
@@ -74,6 +80,28 @@ export default class Decryption {
|
||||
) as Output<TOutputFormat>;
|
||||
}
|
||||
|
||||
static decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
sodium: ISodium,
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat: TOutputFormat = "text" as TOutputFormat
|
||||
): Output<TOutputFormat> {
|
||||
const input = this.transformInput(sodium, cipherData);
|
||||
const plaintext = sodium.crypto_box_seal_open(
|
||||
input,
|
||||
sodium.from_base64(keyPair.publicKey),
|
||||
sodium.from_base64(keyPair.privateKey)
|
||||
);
|
||||
|
||||
return (
|
||||
outputFormat === "base64"
|
||||
? sodium.to_base64(plaintext, base64_variants.URLSAFE_NO_PADDING)
|
||||
: outputFormat === "text"
|
||||
? sodium.to_string(plaintext)
|
||||
: plaintext
|
||||
) as Output<TOutputFormat>;
|
||||
}
|
||||
|
||||
static createStream(
|
||||
sodium: ISodium,
|
||||
header: string,
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
SerializedKeyPair,
|
||||
EncryptionKeyPair
|
||||
EncryptionKeyPair,
|
||||
AsymmetricCipher
|
||||
} from "./types.js";
|
||||
|
||||
export class NNCrypto implements INNCrypto {
|
||||
@@ -96,6 +97,20 @@ export class NNCrypto implements INNCrypto {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
async decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat: TOutputFormat = "text" as TOutputFormat
|
||||
): Promise<Output<TOutputFormat>> {
|
||||
await this.init();
|
||||
return Decryption.decryptAsymmetric(
|
||||
this.sodium,
|
||||
keyPair,
|
||||
cipherData,
|
||||
outputFormat
|
||||
);
|
||||
}
|
||||
|
||||
async hash(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
return Password.hash(this.sodium, password, salt);
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
Output,
|
||||
Input,
|
||||
EncryptionKeyPair,
|
||||
SerializedKeyPair
|
||||
SerializedKeyPair,
|
||||
AsymmetricCipher
|
||||
} from "./types.js";
|
||||
|
||||
export interface IStreamable {
|
||||
@@ -61,6 +62,12 @@ export interface INNCrypto {
|
||||
outputFormat?: TOutputFormat
|
||||
): Promise<Output<TOutputFormat>[]>;
|
||||
|
||||
decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat?: TOutputFormat
|
||||
): Promise<Output<TOutputFormat>>;
|
||||
|
||||
hash(password: string, salt: string): Promise<string>;
|
||||
|
||||
deriveKey(password: string, salt?: string): Promise<EncryptionKey>;
|
||||
|
||||
@@ -17,7 +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 { ISodium } from "@notesnook/sodium";
|
||||
import { base64_variants, ISodium } from "@notesnook/sodium";
|
||||
import {
|
||||
EncryptionKey,
|
||||
EncryptionKeyPair,
|
||||
|
||||
@@ -30,6 +30,11 @@ export type Cipher<TFormat extends DataFormat> = {
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type AsymmetricCipher<TFormat extends DataFormat> = Omit<
|
||||
Cipher<TFormat>,
|
||||
"iv" | "salt"
|
||||
>;
|
||||
|
||||
export type Output<TFormat extends DataFormat> =
|
||||
TFormat extends StringOutputFormat ? string : Uint8Array;
|
||||
export type Input<TFormat extends DataFormat> = Output<TFormat>;
|
||||
|
||||
1
packages/editor-mobile/package-lock.json
generated
1
packages/editor-mobile/package-lock.json
generated
@@ -130,6 +130,7 @@
|
||||
"happy-dom": "16.0.1",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"nodemon": "^3.1.7",
|
||||
"prismjs": "^1.30.0",
|
||||
"prosemirror-test-builder": "^1.1.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -45,6 +45,88 @@
|
||||
#statusbar p {
|
||||
font-family: "Inter";
|
||||
}
|
||||
|
||||
.ProseMirror h1,
|
||||
.ProseMirror h2,
|
||||
.ProseMirror h3,
|
||||
.ProseMirror h4,
|
||||
.ProseMirror h5,
|
||||
.ProseMirror h6 {
|
||||
padding-right: 35px;
|
||||
}
|
||||
|
||||
.ProseMirror h1::before,
|
||||
.ProseMirror h2::before {
|
||||
width: 25px !important;
|
||||
height: 25px !important;
|
||||
background-size: 25 !important;
|
||||
}
|
||||
|
||||
.ProseMirror h3::before,
|
||||
.ProseMirror h4::before {
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
background-size: 22 !important;
|
||||
}
|
||||
|
||||
.ProseMirror h5::before,
|
||||
.ProseMirror h6::before {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
background-size: 20 !important;
|
||||
}
|
||||
|
||||
.ProseMirror h1::before,
|
||||
.ProseMirror h2::before,
|
||||
.ProseMirror h3::before,
|
||||
.ProseMirror h4::before,
|
||||
.ProseMirror h5::before,
|
||||
.ProseMirror h6::before {
|
||||
right: 10px !important;
|
||||
left: unset !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.ProseMirror h1[dir="rtl"]::after,
|
||||
.ProseMirror h2[dir="rtl"]::after,
|
||||
.ProseMirror h3[dir="rtl"]::after,
|
||||
.ProseMirror h4[dir="rtl"]::after,
|
||||
.ProseMirror h5[dir="rtl"]::after,
|
||||
.ProseMirror h6[dir="rtl"]::after {
|
||||
left: 10px !important;
|
||||
right: unset !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.ProseMirror h1[dir="rtl"],
|
||||
.ProseMirror h2[dir="rtl"],
|
||||
.ProseMirror h3[dir="rtl"],
|
||||
.ProseMirror h4[dir="rtl"],
|
||||
.ProseMirror h5[dir="rtl"],
|
||||
.ProseMirror h6[dir="rtl"] {
|
||||
padding-right: 0px;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.ProseMirror h1[data-collapsed="true"]::before,
|
||||
.ProseMirror h2[data-collapsed="true"]::before,
|
||||
.ProseMirror h3[data-collapsed="true"]::before,
|
||||
.ProseMirror h4[data-collapsed="true"]::before,
|
||||
.ProseMirror h5[data-collapsed="true"]::before,
|
||||
.ProseMirror h6[data-collapsed="true"]::before {
|
||||
transform: rotate(90deg) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ProseMirror h1[data-collapsed="true"]::after,
|
||||
.ProseMirror h2[data-collapsed="true"]::after,
|
||||
.ProseMirror h3[data-collapsed="true"]::after,
|
||||
.ProseMirror h4[data-collapsed="true"]::after,
|
||||
.ProseMirror h5[data-collapsed="true"]::after,
|
||||
.ProseMirror h6[data-collapsed="true"]::after {
|
||||
transform: rotate(-90deg) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
<meta name="description" content="Notesnook editor for mobile" />
|
||||
<!--
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function TiptapEditorWrapper(props: {
|
||||
sx={{
|
||||
display: props.settings.noToolbar ? "none" : "flex",
|
||||
overflowY: "hidden",
|
||||
minHeight: "40px",
|
||||
minHeight: "45px",
|
||||
...(globalThis.__PLATFORM__ === "ios" && {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
|
||||
1432
packages/editor/package-lock.json
generated
1432
packages/editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook-importer/core": "^2.2.2",
|
||||
"@notesnook-importer/core": "^2.2.5",
|
||||
"@notesnook/common": "file:../common",
|
||||
"@notesnook/intl": "file:../intl",
|
||||
"@notesnook/theme": "file:../theme",
|
||||
|
||||
@@ -16,7 +16,10 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { getParentAttributes } from "../../utils/prosemirror.js";
|
||||
import {
|
||||
getParentAttributes,
|
||||
isClickWithinBounds
|
||||
} from "../../utils/prosemirror.js";
|
||||
import {
|
||||
InputRule,
|
||||
Node,
|
||||
@@ -235,37 +238,9 @@ export const Callout = Node.create({
|
||||
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
|
||||
const { x, y, width } = e.target.getBoundingClientRect();
|
||||
|
||||
const clientX =
|
||||
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
|
||||
const clientY =
|
||||
e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
||||
|
||||
const hitArea = { width: 40, height: 40 };
|
||||
|
||||
const isRtl =
|
||||
e.target.dir === "rtl" ||
|
||||
findParentNodeClosestToPos(
|
||||
resolvedPos,
|
||||
(node) => !!node.attrs.textDirection
|
||||
)?.node.attrs.textDirection === "rtl";
|
||||
|
||||
let xEnd = clientX <= x + width;
|
||||
let xStart = clientX >= x + width - hitArea.width;
|
||||
|
||||
const yStart = clientY >= y;
|
||||
const yEnd = clientY <= y + hitArea.height;
|
||||
|
||||
if (isRtl) {
|
||||
xStart = clientX >= x;
|
||||
xEnd = clientX <= x + hitArea.width;
|
||||
}
|
||||
|
||||
if (xStart && xEnd && yStart && yEnd) {
|
||||
if (isClickWithinBounds(e, resolvedPos, "right")) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
@@ -283,6 +258,12 @@ export const Callout = Node.create({
|
||||
container.onmousedown = onClick;
|
||||
container.ontouchstart = onClick;
|
||||
|
||||
if (node.attrs.hiddenUnder) {
|
||||
container.dataset.hiddenUnder = node.attrs.hiddenUnder;
|
||||
} else {
|
||||
delete container.dataset.hiddenUnder;
|
||||
}
|
||||
|
||||
return {
|
||||
dom: container,
|
||||
contentDOM: container,
|
||||
@@ -294,6 +275,10 @@ export const Callout = Node.create({
|
||||
if (updatedNode.attrs.collapsed) container.classList.add("collapsed");
|
||||
else container.classList.remove("collapsed");
|
||||
|
||||
if (updatedNode.attrs.hiddenUnder)
|
||||
container.dataset.hiddenUnder = updatedNode.attrs.hiddenUnder;
|
||||
else delete container.dataset.hiddenUnder;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,8 +17,14 @@ 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 { keybindings } from "@notesnook/common";
|
||||
import { KeyboardShortcutCommand, mergeAttributes, Node } from "@tiptap/core";
|
||||
import {
|
||||
findParentNodeClosestToPos,
|
||||
KeyboardShortcutCommand,
|
||||
mergeAttributes,
|
||||
Node
|
||||
} from "@tiptap/core";
|
||||
import { Node as ProseMirrorNode } from "@tiptap/pm/model";
|
||||
import { CheckList } from "../check-list/check-list.js";
|
||||
|
||||
export interface CheckListItemOptions {
|
||||
onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
|
||||
@@ -97,94 +103,76 @@ export const CheckListItem = Node.create<CheckListItemOptions>({
|
||||
|
||||
addNodeView() {
|
||||
return ({ node, getPos, editor }) => {
|
||||
const listItem = document.createElement("li");
|
||||
const checkboxWrapper = document.createElement("label");
|
||||
const checkboxStyler = document.createElement("span");
|
||||
const checkbox = document.createElement("input");
|
||||
const content = document.createElement("div");
|
||||
const li = document.createElement("li");
|
||||
if (node.attrs.checked) li.classList.add("checked");
|
||||
else li.classList.remove("checked");
|
||||
|
||||
checkboxWrapper.contentEditable = "false";
|
||||
checkbox.type = "checkbox";
|
||||
function onClick(e: MouseEvent | TouchEvent) {
|
||||
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
|
||||
checkbox.addEventListener("mousedown", (event) => {
|
||||
if (globalThis.keyboardShown) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
|
||||
checkbox.addEventListener("change", (event) => {
|
||||
event.preventDefault();
|
||||
// if the editor isn’t editable and we don't have a handler for
|
||||
// readonly checks we have to undo the latest change
|
||||
if (!editor.isEditable && !this.options.onReadOnlyChecked) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
const { x, y, right } = li.getBoundingClientRect();
|
||||
|
||||
return;
|
||||
const clientX =
|
||||
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
|
||||
const clientY =
|
||||
e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
||||
|
||||
const hitArea = { width: 40, height: 40 };
|
||||
|
||||
const isRtl =
|
||||
e.target.dir === "rtl" ||
|
||||
findParentNodeClosestToPos(
|
||||
resolvedPos,
|
||||
(node) => !!node.attrs.textDirection
|
||||
)?.node.attrs.textDirection === "rtl";
|
||||
|
||||
let xStart = clientX >= x - hitArea.width;
|
||||
let xEnd = clientX <= x;
|
||||
const yStart = clientY >= y;
|
||||
const yEnd = clientY <= y + hitArea.height;
|
||||
|
||||
if (isRtl) {
|
||||
xEnd = clientX <= right + hitArea.width;
|
||||
xStart = clientX >= right;
|
||||
}
|
||||
|
||||
const { checked } = event.target as any;
|
||||
|
||||
if (editor.isEditable && typeof getPos === "function") {
|
||||
editor
|
||||
.chain()
|
||||
.command(({ tr }) => {
|
||||
const position = getPos();
|
||||
const currentNode = tr.doc.nodeAt(position);
|
||||
|
||||
tr.setNodeMarkup(position, undefined, {
|
||||
...currentNode?.attrs,
|
||||
checked
|
||||
});
|
||||
|
||||
return true;
|
||||
})
|
||||
.run();
|
||||
if (xStart && xEnd && yStart && yEnd) {
|
||||
e.preventDefault();
|
||||
editor.commands.command(({ tr }) => {
|
||||
tr.setNodeAttribute(
|
||||
pos,
|
||||
"checked",
|
||||
!li.classList.contains("checked")
|
||||
);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (!editor.isEditable && this.options.onReadOnlyChecked) {
|
||||
// Reset state if onReadOnlyChecked returns false
|
||||
if (!this.options.onReadOnlyChecked(node, checked)) {
|
||||
checkbox.checked = !checkbox.checked;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (node.attrs.checked) {
|
||||
checkbox.setAttribute("checked", "checked");
|
||||
}
|
||||
|
||||
checkboxWrapper.append(checkbox, checkboxStyler);
|
||||
listItem.append(checkboxWrapper, content);
|
||||
li.onmousedown = onClick;
|
||||
li.ontouchstart = onClick;
|
||||
|
||||
return {
|
||||
dom: listItem,
|
||||
contentDOM: content,
|
||||
dom: li,
|
||||
contentDOM: li,
|
||||
update: (updatedNode) => {
|
||||
if (updatedNode.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
const isNested = updatedNode.lastChild?.type.name === CheckList.name;
|
||||
|
||||
listItem.dataset.checked = updatedNode.attrs.checked;
|
||||
if (updatedNode.attrs.checked) {
|
||||
checkbox.setAttribute("checked", "checked");
|
||||
} else {
|
||||
checkbox.removeAttribute("checked");
|
||||
}
|
||||
if (updatedNode.attrs.checked) li.classList.add("checked");
|
||||
else li.classList.remove("checked");
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// addInputRules() {
|
||||
// return [
|
||||
// wrappingInputRule({
|
||||
// find: inputRegex,
|
||||
// type: this.type,
|
||||
// getAttributes: (match) => ({
|
||||
// checked: match[match.length - 1] === "x"
|
||||
// })
|
||||
// })
|
||||
// ];
|
||||
// }
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||
convertGoogleDocsChecklist(dom);
|
||||
formatCodeblocks(dom);
|
||||
convertBrToSingleSpacedParagraphs(dom);
|
||||
removeImages(dom);
|
||||
removePremiumFeatures(dom);
|
||||
removeBlockId(dom);
|
||||
}
|
||||
return super.parseSlice(dom, options);
|
||||
@@ -53,7 +53,7 @@ export class ClipboardDOMParser extends ProsemirrorDOMParser {
|
||||
convertGoogleDocsChecklist(dom);
|
||||
formatCodeblocks(dom);
|
||||
convertBrToSingleSpacedParagraphs(dom);
|
||||
removeImages(dom);
|
||||
removePremiumFeatures(dom);
|
||||
removeBlockId(dom);
|
||||
}
|
||||
return super.parse(dom, options);
|
||||
@@ -154,11 +154,29 @@ export function convertGoogleDocsChecklist(dom: HTMLElement | Document) {
|
||||
}
|
||||
}
|
||||
|
||||
export function removeImages(dom: HTMLElement | Document) {
|
||||
let canInsertImages: boolean | null = null;
|
||||
for (const img of dom.querySelectorAll(`img`)) {
|
||||
canInsertImages = canInsertImages ?? hasPermission("insertImage");
|
||||
if (!canInsertImages) img.remove();
|
||||
const premiumFeatures = [
|
||||
{
|
||||
selector: "div.callout",
|
||||
permission: "setCallout"
|
||||
},
|
||||
{
|
||||
selector: 'ul[data-type="outlineList"]',
|
||||
permission: "toggleOutlineList"
|
||||
},
|
||||
{
|
||||
selector: "ul.checklist",
|
||||
permission: "toggleTaskList"
|
||||
}
|
||||
] as const;
|
||||
|
||||
function removePremiumFeatures(dom: HTMLElement | Document) {
|
||||
for (const feature of premiumFeatures) {
|
||||
const elements = dom.querySelectorAll(feature.selector);
|
||||
if (elements.length === 0) continue;
|
||||
|
||||
if (!hasPermission(feature.permission)) {
|
||||
elements.forEach((element) => element.remove());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -554,7 +554,8 @@ export const CodeBlock = Node.create<CodeBlockOptions>({
|
||||
return (
|
||||
compareCaretPosition(prev.caretPosition, next.caretPosition) ||
|
||||
prev.language !== next.language ||
|
||||
prev.indentType !== next.indentType
|
||||
prev.indentType !== next.indentType ||
|
||||
prev.hiddenUnder !== next.hiddenUnder
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import { CodeBlockAttributes } from "./code-block.js";
|
||||
import Languages from "./languages.json";
|
||||
import { useThemeEngineStore } from "@notesnook/theme";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import SimpleBar from "simplebar-react";
|
||||
|
||||
export function CodeblockComponent(
|
||||
props: ReactNodeViewProps<CodeBlockAttributes>
|
||||
@@ -58,45 +59,53 @@ export function CodeblockComponent(
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
ref={(ref) => {
|
||||
elementRef.current = ref ?? undefined;
|
||||
forwardRef?.(ref);
|
||||
<SimpleBar
|
||||
style={{
|
||||
backgroundColor: "var(--background-secondary)"
|
||||
}}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
css={theme.codeBlockCSS}
|
||||
sx={{
|
||||
pre: {
|
||||
fontFamily: "inherit !important",
|
||||
tabSize: "inherit !important",
|
||||
// background: "transparent !important",
|
||||
padding: "10px !important",
|
||||
margin: "0px !important",
|
||||
width: "100%",
|
||||
borderRadius: `0px !important`,
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
ref={(ref) => {
|
||||
elementRef.current = ref ?? undefined;
|
||||
forwardRef?.(ref);
|
||||
}}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="none"
|
||||
css={theme.codeBlockCSS}
|
||||
sx={{
|
||||
pre: {
|
||||
fontFamily: "inherit !important",
|
||||
tabSize: "inherit !important",
|
||||
// background: "transparent !important",
|
||||
padding: "10px !important",
|
||||
margin: "0px !important",
|
||||
width: "100%",
|
||||
borderRadius: `0px !important`,
|
||||
|
||||
"::selection,*::selection": {
|
||||
bg: "background-selected",
|
||||
color: "inherit"
|
||||
},
|
||||
"::-moz-selection,*::-moz-selection": {
|
||||
bg: "background-selected",
|
||||
color: "inherit"
|
||||
}
|
||||
},
|
||||
fontFamily: "monospace",
|
||||
whiteSpace: "pre", // TODO !important
|
||||
tabSize: 1,
|
||||
position: "relative",
|
||||
lineHeight: "20px",
|
||||
// bg: "var(--background-secondary)",
|
||||
// color: "white",
|
||||
overflowX: "hidden",
|
||||
display: "flex"
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
"::selection,*::selection": {
|
||||
bg: "background-selected",
|
||||
color: "inherit"
|
||||
},
|
||||
"::-moz-selection,*::-moz-selection": {
|
||||
bg: "background-selected",
|
||||
color: "inherit"
|
||||
}
|
||||
},
|
||||
fontFamily: "monospace",
|
||||
whiteSpace: "pre", // TODO !important
|
||||
tabSize: 1,
|
||||
position: "relative",
|
||||
lineHeight: "20px",
|
||||
// bg: "var(--background-secondary)",
|
||||
// color: "white",
|
||||
// overflowX: "hidden",
|
||||
display: "flex"
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
</SimpleBar>
|
||||
<Flex
|
||||
ref={toolbarRef}
|
||||
contentEditable={false}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`collapse heading > heading collapsed 1`] = `"<h1 data-collapsed="true">Main Heading</h1><p>paragraph.</p><h2>Subheading</h2><p>subheading paragraph</p><h1>Main heading 2</h1><p>paragraph another</p>"`;
|
||||
|
||||
exports[`collapse heading > heading uncollapsed 1`] = `"<h1>Main Heading</h1><p>paragraph.</p><h2>Subheading</h2><p>subheading paragraph</p><h1>Main heading 2</h1><p>paragraph another</p>"`;
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test, expect } from "vitest";
|
||||
import { createEditor } from "../../../../test-utils/index.js";
|
||||
import { Heading } from "../heading.js";
|
||||
|
||||
test("collapse heading", () => {
|
||||
const { editor } = createEditor({
|
||||
extensions: {
|
||||
heading: Heading.configure({ levels: [1, 2, 3, 4, 5, 6] })
|
||||
},
|
||||
initialContent: `
|
||||
<h1>Main Heading</h1>
|
||||
<p>paragraph.</p>
|
||||
<h2>Subheading</h2>
|
||||
<p>subheading paragraph</p>
|
||||
<h1>Main heading 2</h1>
|
||||
<p>paragraph another</p>
|
||||
`
|
||||
});
|
||||
|
||||
const headingPos = 0;
|
||||
|
||||
editor.commands.command(({ tr }) => {
|
||||
tr.setNodeAttribute(headingPos, "collapsed", true);
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(editor.getHTML()).toMatchSnapshot("heading collapsed");
|
||||
|
||||
editor.commands.command(({ tr }) => {
|
||||
tr.setNodeAttribute(headingPos, "collapsed", false);
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(editor.getHTML()).toMatchSnapshot("heading uncollapsed");
|
||||
});
|
||||
@@ -18,11 +18,50 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
import { textblockTypeInputRule } from "@tiptap/core";
|
||||
import {
|
||||
findParentNodeClosestToPos,
|
||||
textblockTypeInputRule
|
||||
} from "@tiptap/core";
|
||||
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
|
||||
import { isClickWithinBounds } from "../../utils/prosemirror.js";
|
||||
import { Selection, Transaction } from "@tiptap/pm/state";
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
|
||||
|
||||
const COLLAPSIBLE_BLOCK_TYPES = [
|
||||
"paragraph",
|
||||
"heading",
|
||||
"blockquote",
|
||||
"bulletList",
|
||||
"orderedList",
|
||||
"checkList",
|
||||
"taskList",
|
||||
"table",
|
||||
"callout",
|
||||
"codeblock",
|
||||
"image",
|
||||
"outlineList",
|
||||
"mathBlock",
|
||||
"webclip",
|
||||
"embed"
|
||||
];
|
||||
|
||||
const HEADING_REGEX = /^(#{1,6})\s$/;
|
||||
export const Heading = TiptapHeading.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
collapsed: {
|
||||
default: false,
|
||||
keepOnSplit: false,
|
||||
parseHTML: (element) => element.dataset.collapsed === "true",
|
||||
renderHTML: (attributes) => ({
|
||||
"data-collapsed": attributes.collapsed === true
|
||||
})
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
@@ -45,17 +84,65 @@ export const Heading = TiptapHeading.extend({
|
||||
};
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return this.options.levels.reduce(
|
||||
(items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
[tiptapKeys[`insertHeading${level}`].keys]: () =>
|
||||
this.editor.commands.setHeading({ level })
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: COLLAPSIBLE_BLOCK_TYPES,
|
||||
attributes: {
|
||||
hiddenUnder: {
|
||||
default: null,
|
||||
keepOnSplit: false,
|
||||
parseHTML: (element) => element.dataset.hiddenUnder || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.hiddenUnder) return {};
|
||||
return {
|
||||
"data-hidden-under": attributes.hiddenUnder
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
...this.options.levels.reduce(
|
||||
(items, level) => ({
|
||||
...items,
|
||||
...{
|
||||
[tiptapKeys[`insertHeading${level}`].keys]: () =>
|
||||
this.editor.commands.setHeading({ level })
|
||||
}
|
||||
}),
|
||||
{}
|
||||
),
|
||||
Enter: ({ editor }) => {
|
||||
const { state, commands } = editor;
|
||||
const { $from } = state.selection;
|
||||
const node = $from.node();
|
||||
if (node.type.name !== this.name) return false;
|
||||
|
||||
const isAtEnd = $from.parentOffset === node.textContent.length;
|
||||
if (isAtEnd && node.attrs.collapsed) {
|
||||
const headingPos = $from.before();
|
||||
const endPos = findEndOfCollapsedSection(
|
||||
state.doc,
|
||||
headingPos,
|
||||
node.attrs.level
|
||||
);
|
||||
if (endPos === -1) return false;
|
||||
|
||||
return commands.command(({ tr }) => {
|
||||
tr.insert(endPos, state.schema.nodes.paragraph.create());
|
||||
const newPos = endPos + 1;
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(newPos)));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
addInputRules() {
|
||||
@@ -71,5 +158,177 @@ export const Heading = TiptapHeading.extend({
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ({ node, getPos, editor, HTMLAttributes }) => {
|
||||
const heading = document.createElement(`h${node.attrs.level}`);
|
||||
|
||||
for (const attr in HTMLAttributes) {
|
||||
heading.setAttribute(attr, HTMLAttributes[attr]);
|
||||
}
|
||||
|
||||
if (node.attrs.collapsed) heading.dataset.collapsed = "true";
|
||||
else delete heading.dataset.collapsed;
|
||||
|
||||
function onClick(e: MouseEvent | TouchEvent) {
|
||||
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||
if (!(e.target instanceof HTMLHeadingElement)) return;
|
||||
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
const calloutAncestor = findParentNodeClosestToPos(
|
||||
resolvedPos,
|
||||
(node) => node.type.name === "callout"
|
||||
);
|
||||
if (calloutAncestor) return;
|
||||
|
||||
if (
|
||||
isClickWithinBounds(
|
||||
e,
|
||||
resolvedPos,
|
||||
useToolbarStore.getState().isMobile ? "right" : "left"
|
||||
)
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
editor.commands.command(({ tr }) => {
|
||||
const currentNode = tr.doc.nodeAt(pos);
|
||||
if (currentNode && currentNode.type.name === "heading") {
|
||||
const shouldCollapse = !currentNode.attrs.collapsed;
|
||||
const headingLevel = currentNode.attrs.level;
|
||||
const headingId = currentNode.attrs.blockId;
|
||||
|
||||
tr.setNodeAttribute(pos, "collapsed", shouldCollapse);
|
||||
toggleNodesUnderHeading(
|
||||
tr,
|
||||
pos,
|
||||
headingLevel,
|
||||
shouldCollapse,
|
||||
headingId
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
heading.onmousedown = onClick;
|
||||
heading.ontouchstart = onClick;
|
||||
|
||||
return {
|
||||
dom: heading,
|
||||
contentDOM: heading,
|
||||
update: (updatedNode) => {
|
||||
if (updatedNode.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updatedNode.attrs.level !== node.attrs.level) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updatedNode.attrs.collapsed) heading.dataset.collapsed = "true";
|
||||
else delete heading.dataset.collapsed;
|
||||
|
||||
if (updatedNode.attrs.hiddenUnder)
|
||||
heading.dataset.hiddenUnder = updatedNode.attrs.hiddenUnder;
|
||||
else delete heading.dataset.hiddenUnder;
|
||||
|
||||
if (updatedNode.attrs.textAlign)
|
||||
heading.style.textAlign =
|
||||
updatedNode.attrs.textAlign === "left"
|
||||
? ""
|
||||
: updatedNode.attrs.textAlign;
|
||||
|
||||
if (updatedNode.attrs.textDirection)
|
||||
heading.dir = updatedNode.attrs.textDirection;
|
||||
else heading.dir = "";
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function toggleNodesUnderHeading(
|
||||
tr: Transaction,
|
||||
headingPos: number,
|
||||
headingLevel: number,
|
||||
isCollapsing: boolean,
|
||||
headingId: string
|
||||
) {
|
||||
const { doc } = tr;
|
||||
const headingNode = doc.nodeAt(headingPos);
|
||||
if (!headingNode || headingNode.type.name !== "heading") return;
|
||||
|
||||
let nextPos = headingPos + headingNode.nodeSize;
|
||||
const cursorPos = tr.selection.from;
|
||||
let shouldMoveCursor = false;
|
||||
|
||||
while (nextPos < doc.content.size) {
|
||||
const nextNode = doc.nodeAt(nextPos);
|
||||
if (!nextNode) break;
|
||||
|
||||
if (
|
||||
nextNode.type.name === "heading" &&
|
||||
nextNode.attrs.level <= headingLevel
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
isCollapsing &&
|
||||
cursorPos >= nextPos &&
|
||||
cursorPos < nextPos + nextNode.nodeSize
|
||||
) {
|
||||
shouldMoveCursor = true;
|
||||
}
|
||||
|
||||
if (COLLAPSIBLE_BLOCK_TYPES.includes(nextNode.type.name)) {
|
||||
if (isCollapsing && typeof nextNode.attrs.hiddenUnder !== "string") {
|
||||
tr.setNodeAttribute(nextPos, "hiddenUnder", headingId);
|
||||
} else if (!isCollapsing && nextNode.attrs.hiddenUnder === headingId) {
|
||||
tr.setNodeAttribute(nextPos, "hiddenUnder", null);
|
||||
}
|
||||
}
|
||||
|
||||
nextPos += nextNode.nodeSize;
|
||||
}
|
||||
|
||||
if (shouldMoveCursor) {
|
||||
const headingEndPos = headingPos + headingNode.nodeSize - 1;
|
||||
tr.setSelection(Selection.near(tr.doc.resolve(headingEndPos)));
|
||||
}
|
||||
}
|
||||
|
||||
function findEndOfCollapsedSection(
|
||||
doc: Node,
|
||||
headingPos: number,
|
||||
headingLevel: number
|
||||
) {
|
||||
const headingNode = doc.nodeAt(headingPos);
|
||||
if (!headingNode || headingNode.type.name !== "heading") return -1;
|
||||
|
||||
let nextPos = headingPos + headingNode.nodeSize;
|
||||
|
||||
while (nextPos < doc.content.size) {
|
||||
const nextNode = doc.nodeAt(nextPos);
|
||||
if (!nextNode) break;
|
||||
|
||||
if (
|
||||
nextNode.type.name === "heading" &&
|
||||
nextNode.attrs.level <= headingLevel
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
nextPos += nextNode.nodeSize;
|
||||
}
|
||||
|
||||
return nextPos;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import "@tiptap/extension-text-style";
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { config } from "../../utils/config.js";
|
||||
import { tiptapKeys } from "@notesnook/common";
|
||||
|
||||
export interface HighlightOptions {
|
||||
types: string[];
|
||||
@@ -99,11 +101,16 @@ export const Highlight = Extension.create<HighlightOptions>({
|
||||
.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// addKeyboardShortcuts() {
|
||||
// return {
|
||||
// "Mod-Shift-h": () => this.editor.commands.toggleHighlight(),
|
||||
// };
|
||||
// },
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
[tiptapKeys.toggleHighlight.keys]: () =>
|
||||
this.editor.commands.toggleHighlight(getCurrentHighlightColor())
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function getCurrentHighlightColor() {
|
||||
return config.get<"string">("highlight") || "yellow";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`migration > inline image in outline list 1`] = `
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "item 1",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "hello",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"align": undefined,
|
||||
"aspectRatio": 1,
|
||||
"filename": undefined,
|
||||
"hash": undefined,
|
||||
"height": null,
|
||||
"mime": undefined,
|
||||
"progress": 0,
|
||||
"size": undefined,
|
||||
"src": "image.png",
|
||||
"type": "image",
|
||||
"width": null,
|
||||
},
|
||||
"type": "image",
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "world",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "sub item 2",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "sub item 3",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
],
|
||||
"type": "outlineList",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "item 4",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
],
|
||||
"type": "outlineList",
|
||||
},
|
||||
],
|
||||
"type": "doc",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`migration > inline image in paragraph 1`] = `
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "hello",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"align": undefined,
|
||||
"aspectRatio": 1,
|
||||
"filename": undefined,
|
||||
"hash": undefined,
|
||||
"height": null,
|
||||
"mime": undefined,
|
||||
"progress": 0,
|
||||
"size": undefined,
|
||||
"src": "image.png",
|
||||
"type": "image",
|
||||
"width": null,
|
||||
},
|
||||
"type": "image",
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "world",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "doc",
|
||||
}
|
||||
`;
|
||||
75
packages/editor/src/extensions/image/__tests__/image.test.ts
Normal file
75
packages/editor/src/extensions/image/__tests__/image.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
createEditor,
|
||||
h,
|
||||
p,
|
||||
outlineList,
|
||||
outlineListItem
|
||||
} from "../../../../test-utils/index.js";
|
||||
import { test, expect, describe } from "vitest";
|
||||
import { ImageNode } from "../index.js";
|
||||
import { OutlineList } from "../../outline-list/outline-list.js";
|
||||
import { OutlineListItem } from "../../outline-list-item/outline-list-item.js";
|
||||
|
||||
describe("migration", () => {
|
||||
test(`inline image in paragraph`, async () => {
|
||||
const el = p(["hello", h("img", [], { src: "image.png" }), "world"]);
|
||||
const {
|
||||
builder: { image },
|
||||
editor
|
||||
} = createEditor({
|
||||
initialContent: el.outerHTML,
|
||||
extensions: {
|
||||
image: ImageNode.configure({})
|
||||
}
|
||||
});
|
||||
|
||||
expect(editor.getJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test(`inline image in outline list`, async () => {
|
||||
const el = outlineList(
|
||||
outlineListItem(["item 1"]),
|
||||
outlineListItem(
|
||||
["hello", h("img", [], { src: "image.png" }), "world"],
|
||||
outlineList(
|
||||
outlineListItem(["sub item 2"]),
|
||||
outlineListItem(["sub item 3"])
|
||||
)
|
||||
),
|
||||
outlineListItem(["item 4"])
|
||||
);
|
||||
|
||||
const {
|
||||
builder: { image },
|
||||
editor
|
||||
} = createEditor({
|
||||
initialContent: el.outerHTML,
|
||||
extensions: {
|
||||
outlineList: OutlineList,
|
||||
outlineListItem: OutlineListItem,
|
||||
image: ImageNode
|
||||
}
|
||||
});
|
||||
|
||||
expect(editor.getJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -131,39 +131,6 @@ export const ImageNode = Node.create<ImageOptions>({
|
||||
getAttrs(node) {
|
||||
if (node.querySelectorAll("img").length <= 0) return false;
|
||||
return null;
|
||||
},
|
||||
getContent: (dom, schema) => {
|
||||
const wrapper = document.createElement("div");
|
||||
let buffer = "";
|
||||
|
||||
const flushBuffer = () => {
|
||||
if (buffer.trim().length > 0) {
|
||||
const pEl = document.createElement("p");
|
||||
pEl.innerHTML = buffer;
|
||||
wrapper.appendChild(pEl);
|
||||
buffer = "";
|
||||
}
|
||||
};
|
||||
|
||||
for (const child of dom.childNodes) {
|
||||
if (
|
||||
child.nodeType === globalThis.Node.ELEMENT_NODE &&
|
||||
(child as HTMLElement).tagName === "IMG"
|
||||
) {
|
||||
flushBuffer();
|
||||
wrapper.appendChild(child);
|
||||
} else {
|
||||
if (child.nodeType === globalThis.Node.ELEMENT_NODE) {
|
||||
buffer += (child as HTMLElement).outerHTML;
|
||||
} else if (child.nodeType === globalThis.Node.TEXT_NODE) {
|
||||
buffer += child.textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
flushBuffer();
|
||||
|
||||
const parser = DOMParser.fromSchema(schema);
|
||||
return parser.parse(wrapper).content;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -125,6 +125,12 @@ export class MathView implements NodeView, ICursorPosObserver {
|
||||
if (options.className) this.dom.classList.add(options.className);
|
||||
this.dom.classList.add("math-node");
|
||||
|
||||
if (node.attrs.hiddenUnder) {
|
||||
this.dom.dataset.hiddenUnder = node.attrs.hiddenUnder;
|
||||
} else {
|
||||
delete this.dom.dataset.hiddenUnder;
|
||||
}
|
||||
|
||||
this._mathRenderElt = document.createElement("span");
|
||||
this._mathRenderElt.textContent = "";
|
||||
this._mathRenderElt.classList.add("math-render");
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`outline list item > code block in outline list item 1`] = `
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "item 1",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "hello",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"caretPosition": undefined,
|
||||
"id": "codeblock-test-id-123456",
|
||||
"indentLength": 2,
|
||||
"indentType": "space",
|
||||
"language": null,
|
||||
"lines": [],
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"text": "const x = 1;",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "codeblock",
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "world",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "sub item 2",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "sub item 3",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
],
|
||||
"type": "outlineList",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
{
|
||||
"attrs": {
|
||||
"collapsed": false,
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "item 4",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"type": "paragraph",
|
||||
},
|
||||
],
|
||||
"type": "outlineListItem",
|
||||
},
|
||||
],
|
||||
"type": "outlineList",
|
||||
},
|
||||
],
|
||||
"type": "doc",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
createEditor,
|
||||
h,
|
||||
li,
|
||||
outlineList,
|
||||
outlineListItem
|
||||
} from "../../../../test-utils/index.js";
|
||||
import { test, expect, describe, beforeAll, vi } from "vitest";
|
||||
import { OutlineList } from "../../outline-list/outline-list.js";
|
||||
import { OutlineListItem } from "../outline-list-item.js";
|
||||
import { CodeBlock } from "../../code-block/code-block.js";
|
||||
|
||||
describe("outline list item", () => {
|
||||
beforeAll(() => {
|
||||
vi.mock("nanoid", () => ({
|
||||
nanoid: () => "test-id-123456"
|
||||
}));
|
||||
});
|
||||
|
||||
test(`code block in outline list item`, async () => {
|
||||
const subList = outlineList(
|
||||
outlineListItem(["sub item 2"]),
|
||||
outlineListItem(["sub item 3"])
|
||||
);
|
||||
const listItemWithCodeBlock = li(
|
||||
[
|
||||
h("p", ["hello"]),
|
||||
h("pre", [h("code", ["const x = 1;"])]),
|
||||
h("p", ["world"]),
|
||||
subList
|
||||
],
|
||||
{ "data-type": "outlineListItem" }
|
||||
);
|
||||
const el = outlineList(
|
||||
outlineListItem(["item 1"]),
|
||||
listItemWithCodeBlock,
|
||||
outlineListItem(["item 4"])
|
||||
);
|
||||
|
||||
const {
|
||||
builder: { codeBlock },
|
||||
editor
|
||||
} = createEditor({
|
||||
initialContent: el.outerHTML,
|
||||
extensions: {
|
||||
outlineList: OutlineList,
|
||||
outlineListItem: OutlineListItem,
|
||||
codeBlock: CodeBlock
|
||||
}
|
||||
});
|
||||
|
||||
expect(editor.getJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -22,9 +22,13 @@ import {
|
||||
mergeAttributes,
|
||||
findParentNodeClosestToPos
|
||||
} from "@tiptap/core";
|
||||
import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror.js";
|
||||
import {
|
||||
findParentNodeOfTypeClosestToPos,
|
||||
isClickWithinBounds
|
||||
} from "../../utils/prosemirror.js";
|
||||
import { OutlineList } from "../outline-list/outline-list.js";
|
||||
import { keybindings, tiptapKeys } from "@notesnook/common";
|
||||
import { Paragraph } from "../paragraph/paragraph.js";
|
||||
|
||||
export interface ListItemOptions {
|
||||
HTMLAttributes: Record<string, unknown>;
|
||||
@@ -52,13 +56,14 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
};
|
||||
},
|
||||
|
||||
content: "paragraph+ list?",
|
||||
content: "paragraph block*",
|
||||
|
||||
defining: true,
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
priority: 100,
|
||||
tag: `li[data-type="${this.name}"]`
|
||||
}
|
||||
];
|
||||
@@ -92,21 +97,37 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
return true;
|
||||
});
|
||||
},
|
||||
Enter: () => {
|
||||
// const subList = findSublist(editor, this.type);
|
||||
// if (!subList) return this.editor.commands.splitListItem(this.name);
|
||||
|
||||
// const { isCollapsed, subListPos } = subList;
|
||||
|
||||
// if (isCollapsed) {
|
||||
// return this.editor.commands.toggleOutlineCollapse(subListPos, false);
|
||||
// }
|
||||
Enter: ({ editor }) => {
|
||||
const { $anchor } = editor.state.selection;
|
||||
if (
|
||||
$anchor.parent.type.name !== Paragraph.name ||
|
||||
($anchor.parent.type.name === Paragraph.name &&
|
||||
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
|
||||
)
|
||||
return false;
|
||||
|
||||
return this.editor.commands.splitListItem(this.name);
|
||||
},
|
||||
Tab: () => this.editor.commands.sinkListItem(this.name),
|
||||
[keybindings.liftListItem.keys]: () =>
|
||||
this.editor.commands.liftListItem(this.name)
|
||||
Tab: ({ editor }) => {
|
||||
const { $anchor } = editor.state.selection;
|
||||
if (
|
||||
$anchor.parent.type.name !== Paragraph.name ||
|
||||
($anchor.parent.type.name === Paragraph.name &&
|
||||
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
|
||||
)
|
||||
return false;
|
||||
return this.editor.commands.sinkListItem(this.name);
|
||||
},
|
||||
[keybindings.liftListItem.keys]: ({ editor }) => {
|
||||
const { $anchor } = editor.state.selection;
|
||||
if (
|
||||
$anchor.parent.type.name !== Paragraph.name ||
|
||||
($anchor.parent.type.name === Paragraph.name &&
|
||||
$anchor.node($anchor.depth - 1)?.type.name !== this.type.name)
|
||||
)
|
||||
return false;
|
||||
return this.editor.commands.liftListItem(this.name);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
@@ -124,41 +145,14 @@ export const OutlineListItem = Node.create<ListItemOptions>({
|
||||
|
||||
function onClick(e: MouseEvent | TouchEvent) {
|
||||
if (e instanceof MouseEvent && e.button !== 0) return;
|
||||
if (!(e.target instanceof HTMLParagraphElement)) return;
|
||||
if (!(e.target instanceof HTMLElement)) return;
|
||||
if (!li.classList.contains("nested")) return;
|
||||
|
||||
const pos = typeof getPos === "function" ? getPos() : 0;
|
||||
if (typeof pos !== "number") return;
|
||||
|
||||
const resolvedPos = editor.state.doc.resolve(pos);
|
||||
|
||||
const { x, y, right } = li.getBoundingClientRect();
|
||||
|
||||
const clientX =
|
||||
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
|
||||
|
||||
const clientY =
|
||||
e instanceof MouseEvent ? e.clientY : e.touches[0].clientY;
|
||||
|
||||
const hitArea = { width: 40, height: 40 };
|
||||
|
||||
const isRtl =
|
||||
e.target.dir === "rtl" ||
|
||||
findParentNodeClosestToPos(
|
||||
resolvedPos,
|
||||
(node) => !!node.attrs.textDirection
|
||||
)?.node.attrs.textDirection === "rtl";
|
||||
|
||||
let xStart = clientX >= x - hitArea.width;
|
||||
let xEnd = clientX <= x;
|
||||
const yStart = clientY >= y;
|
||||
const yEnd = clientY <= y + hitArea.height;
|
||||
|
||||
if (isRtl) {
|
||||
xEnd = clientX <= right + hitArea.width;
|
||||
xStart = clientX >= right;
|
||||
}
|
||||
|
||||
if (xStart && xEnd && yStart && yEnd) {
|
||||
if (isClickWithinBounds(e, resolvedPos, "left")) {
|
||||
e.preventDefault();
|
||||
editor.commands.command(({ tr }) => {
|
||||
tr.setNodeAttribute(
|
||||
|
||||
@@ -112,6 +112,12 @@ export class ReactNodeView<P extends ReactNodeViewProps> implements NodeView {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.node.attrs.hiddenUnder) {
|
||||
this.domRef.dataset.hiddenUnder = this.node.attrs.hiddenUnder;
|
||||
} else {
|
||||
delete this.domRef.dataset.hiddenUnder;
|
||||
}
|
||||
|
||||
portalProviderAPI.render(this.Component, this.domRef);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user