Compare commits

...

84 Commits

Author SHA1 Message Date
Abdullah Atta
efec2dc898 Merge pull request #8803 from streetwriters/release/3.3.2
Release v3.3.2
2025-10-20 16:05:15 +05:00
Ammar Ahmed
5bd032e480 mobile: release v3.3.5 2025-10-20 13:53:14 +05:00
Abdullah Atta
c1bd4cfb01 web: bump version to 3.3.2 2025-10-20 13:01:57 +05:00
Abdullah Atta
0b943d8ecd editor: fix bullet list created as sub list of outline list 2025-10-20 12:52:57 +05:00
Ammar Ahmed
75e22a591e Merge pull request #8802 from streetwriters/fix-reset-root-state
Fix reset root state
2025-10-20 12:44:13 +05:00
Ammar Ahmed
66498e12ce mobile: fix multiple timers set on reset root state causing jank 2025-10-20 12:40:20 +05:00
Ammar Ahmed
d5fefd2f10 Merge pull request #8801 from streetwriters/fix-collapsible-headings
Fix collapsible headings on mobile
2025-10-20 12:12:20 +05:00
Ammar Ahmed
e2556d3228 mobile: fix collapsible heading 2025-10-20 12:09:22 +05:00
Abdullah Atta
2eb38d236e web: allow discounts in overlay checkout 2025-10-20 11:00:26 +05:00
Abdullah Atta
d9c57162f1 Merge pull request #8800 from streetwriters/fix/allow-discounts-in-overlay-checkout
Allow discounts in overlay checkout
2025-10-20 10:59:11 +05:00
Abdullah Atta
9f8f0e8413 web: allow discounts in overlay checkout 2025-10-20 10:57:51 +05:00
Abdullah Atta
ea52893be1 Merge branch 'master' into beta 2025-10-20 10:43:07 +05:00
Abdullah Atta
eef63d2028 Merge pull request #8799 from streetwriters/fix/entities-in-markdown-import
Fix html entities in markdown import
2025-10-20 10:37:32 +05:00
Abdullah Atta
14e0112e17 Merge pull request #8798 from streetwriters/fix/handle-attachment-errors-during-export
Handle attachment errors on export
2025-10-20 10:26:56 +05:00
Abdullah Atta
8ec9034fb2 Merge pull request #8797 from streetwriters/fix/disable-in-app-notices
Disable in-app noticies (temporarily)
2025-10-20 10:26:35 +05:00
Abdullah Atta
dc5973d75d editor: update package-lock file 2025-10-20 10:24:23 +05:00
Abdullah Atta
12126b2f9f web: fix html entities in markdown import 2025-10-20 10:17:59 +05:00
Abdullah Atta
9fa256be0e web: handle attachment errors on export 2025-10-20 08:58:22 +05:00
Abdullah Atta
c103328098 web: disable in-app noticies (temporarily) 2025-10-20 08:52:22 +05:00
Ammar Ahmed
ea2022bbe4 Merge branch 'mobile-release-v3.3.4' into beta 2025-10-20 08:39:56 +05:00
Ammar Ahmed
d7ced57319 Merge pull request #8796 from streetwriters/mobile-release-v3.3.4
Mobile release v3.3.4
2025-10-20 08:38:33 +05:00
Ammar Ahmed
2cc200dc2b mobile: fix paths for fonts 2025-10-18 13:35:23 +05:00
Ammar Ahmed
baa74d0353 mobile: fix build error due to tsgo usage 2025-10-18 12:56:40 +05:00
Ammar Ahmed
72b906da14 mobile: use currency symbol when possible 2025-10-18 12:15:30 +05:00
Ammar Ahmed
6db8c2fac2 mobile: release v3.3.4 2025-10-18 12:02:00 +05:00
Ammar Ahmed
09d5c0a65e mobile: fix sharing files from other apps to Notesnook 2025-10-18 12:01:19 +05:00
Ammar Ahmed
0b6b6a02cc editor: fix crash when adding new row in column 2025-10-18 09:04:06 +05:00
Abdullah Atta
f49c673e8e editor: fix crash when adding new row in table 2025-10-18 09:00:36 +05:00
Ammar Ahmed
64a8e9a075 mobile: set github release=false 2025-10-18 09:00:12 +05:00
Ammar Ahmed
e998b07ca5 mobile: fix strings 2025-10-18 08:57:54 +05:00
Ammar Ahmed
c189279030 mobile: fix sheet insets on android 16+ 2025-10-18 08:57:54 +05:00
Ammar Ahmed
12ddd6f958 mobile: add release type in github issue template 2025-10-18 08:57:54 +05:00
Ammar Ahmed
da06afda8a mobile: fix pricing on github release 2025-10-18 08:57:54 +05:00
Ammar Ahmed
bf808b7a2e mobile: fix editor toolbar height 2025-10-18 08:57:54 +05:00
Ammar Ahmed
13476a3f50 editor: fix checklist style 2025-10-18 08:56:04 +05:00
Ammar Ahmed
cfdf77a7b5 Merge branch 'fix-plans-showing-price' into beta 2025-10-17 09:59:38 +05:00
Ammar Ahmed
070c90362a Merge pull request #8782 from streetwriters/fix-plans-showing-price
Fix prices on github release and open checkout link in browser
2025-10-17 09:58:29 +05:00
Ammar Ahmed
c8d9858a0c Merge branch 'mobile-release-3.3.3' into beta 2025-10-17 09:11:28 +05:00
Ammar Ahmed
2685f60409 Merge pull request #8781 from streetwriters/mobile-release-3.3.3
mobile: release v3.3.3
2025-10-17 09:10:51 +05:00
Abdullah Atta
8d3bb5185c Merge branch 'master' into beta 2025-10-17 08:51:19 +05:00
Abdullah Atta
1f748a4026 global: introducing Notesnook Circle (#8779)
* core: add api for getting Notesnook Circle partners

* web: introduce notesnook circle

* core: export CirclePartner type

* mobile: add notesnook-circle

* web: use strings from intl for notesnook circle

* common: add notesnook circle to features

* web: fix local ips set for hosts

* mobile: fix db hosts

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2025-10-17 08:49:32 +05:00
Sudlon
b27f064ad4 editor: Add keybindings for highlighting the currently selected text (#8577)
* editor: Add keybindings for highlighting the currently selected text

Signed-off-by: Sudlon <sudlondon@outlook.com>

* editor: Change keybinding command to TipTap "syntax"

Signed-off-by: Sudlon <sudlondon@outlook.com>

* editor: Remove SHIFT-keybinding (leaving ALT-keybinding) for highlighting currently selected text

Signed-off-by: Sudlon sudlondon@outlook.com

* editor: Remove left-over references to the double keybinding for highlighting currently selected text

Signed-off-by: Sudlon sudlondon@outlook.com

---------

Signed-off-by: Sudlon <sudlondon@outlook.com>
Signed-off-by: Sudlon sudlondon@outlook.com
Co-authored-by: sudlon <sudlondon@outlook.com>
2025-10-16 16:59:37 +05:00
01zulfi
27f781cc63 editor: improve codeblock scrollbar styling (#8548)
* editor: improve codeblock scrollbar styling
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix overflowed codeblock's bg color
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-16 16:50:35 +05:00
Abdullah Atta
1427958eca Merge pull request #8457 from 01zulfi/editor/tasklist-checklist-styling
editor: improve checklist styling
2025-10-16 15:01:24 +05:00
Abdullah Atta
436e09c255 editor: fix text direction not working in check lists 2025-10-16 14:50:13 +05:00
Abdullah Atta
cc9b2dd5b0 editor: add back line through for checked items 2025-10-16 14:45:40 +05:00
Abdullah Atta
0ba25419f3 editor: use single li element for simple checklists 2025-10-16 14:43:51 +05:00
Abdullah Atta
9238de4cb4 Merge pull request #8772 from 01zulfi/editor/fix-images-in-outline-lists
editor: fix inserting images in outline lists
2025-10-16 11:48:34 +05:00
01zulfi
2d5fe1058c editor: add test for code block in outline list
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-16 10:46:31 +05:00
01zulfi
b01d1fc87e editor: improve checklist styling
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-16 09:28:02 +05:00
Abdullah Atta
9f4c0a4c9a editor: move outline list item indicator to li from first child 2025-10-16 09:23:19 +05:00
Abdullah Atta
9eebda86c2 editor: fix enter/tab not working in block nodes inside outline list item 2025-10-16 09:22:49 +05:00
Abdullah Atta
d43e856fea editor: add support for all block types in outline list item 2025-10-16 09:22:15 +05:00
Abdullah Atta
7af1596038 editor: remove custom inline image -> block image migration 2025-10-16 09:21:45 +05:00
01zulfi
652f6eab0b editor: fix inserting images in outline lists
* inserting images in outline list was splitting the outline list into two, this commit fixes it

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-15 11:14:24 +05:00
Abdullah Atta
fdaddfdb92 Merge pull request #8767 from streetwriters/chore/update-electron-to-37 2025-10-13 14:12:34 +05:00
Abdullah Atta
9a1adf2065 Merge pull request #8767 from streetwriters/chore/update-electron-to-37
Update electron to v37
2025-10-13 14:12:01 +05:00
Abdullah Atta
f10b258f58 desktop: update electron to v37 2025-10-13 14:10:52 +05:00
01zulfi
7f558cbd41 core: handle inbox item sync & decryption (#8733)
* core: handle inbox item sync & decryption
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: minor refactors in handling inbox item sync
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: use inbox item salt
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: check inbox item version
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-13 14:02:32 +05:00
Ammar Ahmed
641eb28ce7 mobile: open web purchase in browser. 2025-10-13 11:33:48 +05:00
Ammar Ahmed
2c59a5f6f9 mobile: fix plan price not showing properly on gh release 2025-10-13 09:30:48 +05:00
Abdullah Atta
712f02e61e Merge branch 'master' into beta 2025-10-10 18:13:46 +05:00
Abdullah Atta
e5ad405a23 Merge pull request #8754 from streetwriters/release/3.3.1
Release v3.3.1
2025-10-10 18:12:40 +05:00
Abdullah Atta
cd1b78d8df ci: fix desktop builds 2025-10-10 17:53:41 +05:00
Abdullah Atta
3b1ca46945 web: fix desktop tests 2025-10-10 16:23:11 +05:00
Abdullah Atta
96c1636f88 web: bump version to 3.3.1 2025-10-10 15:54:29 +05:00
Abdullah Atta
a10f949856 Merge branch 'master' into beta 2025-10-10 15:53:32 +05:00
Abdullah Atta
9ff4757ed6 Merge pull request #8753 from streetwriters/fix/make-table-with-adjustable
Make table width adjustable
2025-10-10 15:52:26 +05:00
Abdullah Atta
17c1e77f54 editor: make table width adjustable 2025-10-10 15:47:05 +05:00
Abdullah Atta
187a2a7262 Merge pull request #8735 from 01zulfi/editor/restrict-pasting-premium-features
editor: restrict pasting premium features
2025-10-10 15:38:11 +05:00
Abdullah Atta
4621230549 Merge pull request #8751 from streetwriters/fix/discount-price-on-checkout
Fix discounted price in checkout
2025-10-10 15:33:42 +05:00
Abdullah Atta
d7cb3317a2 web: fix discounted price in checkout 2025-10-10 15:09:41 +05:00
01zulfi
6d81f26864 editor: restrict pasting premium features
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-08 13:31:24 +05:00
01zulfi
5219814b5a Merge pull request #8557 from 01zulfi/editor/refactor-collapsible-headings
editor: make headings collapsible
2025-10-08 13:26:37 +05:00
Abdullah Atta
bef9fd0fd0 Merge branch 'master' into beta 2025-10-08 13:25:42 +05:00
Ammar Ahmed
105fb0fc69 mobile: release v3.3.3 2025-10-06 13:13:42 +05:00
Ammar Ahmed
89c408981c Merge branch 'master' into beta 2025-10-06 12:42:59 +05:00
Ammar Ahmed
8c621f9f47 mobile: add null checks 2025-10-06 09:51:16 +05:00
Ammar Ahmed
74544efd62 mobile: update version to v3.3.2 2025-10-06 09:51:16 +05:00
Ammar Ahmed
c30ccbb68d mobile: fix crash on entering settings 2025-10-06 09:51:16 +05:00
Ammar Ahmed
12e3335d52 mobile: release v3.3.1 2025-10-04 23:57:09 +05:00
Ammar Ahmed
f29439539d mobile: misc bug fixes 2025-10-04 23:57:09 +05:00
Ammar Ahmed
13c9fdba52 mobile: update monograph publish sheet 2025-10-04 23:57:09 +05:00
Abdullah Atta
95f52fba51 Merge pull request #8633 from streetwriters/release/v3.3 2025-10-04 22:08:21 +05:00
113 changed files with 4121 additions and 2618 deletions

View File

@@ -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

View File

@@ -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: |

View File

@@ -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",

View File

@@ -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",

View File

@@ -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`,

View File

@@ -38,9 +38,6 @@ export function hideAuth(context) {
context === "intro"
) {
Navigation.replace("FluidPanelsView");
setTimeout(() => {
Navigation.resetRootState();
}, 1000);
} else {
Navigation.goBack();
}

View File

@@ -244,7 +244,7 @@ const PDFPreview = () => {
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "OpenSans-Regular"
fontFamily: "Inter-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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,

View File

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

View File

@@ -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}

View File

@@ -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"

View File

@@ -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();

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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 />
};

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

View File

@@ -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"
}
]
},

View File

@@ -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

View File

@@ -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";

View File

@@ -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'

View File

@@ -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>

View File

@@ -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!

View File

@@ -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>

View File

@@ -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;

View File

@@ -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">

View File

@@ -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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.2",
"version": "3.3.5",
"private": true,
"license": "GPL-3.0-or-later",
"workspaces": [

View File

@@ -70,6 +70,7 @@ const EXTRA_ICON_NAMES = [
"chevron-down",
"calendar",
"minus-circle-outline",
"circle-outline",
"close-circle-outline",
"qrcode",
"text",

View File

@@ -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
}

View File

@@ -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",

View File

@@ -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": {

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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" });

View File

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

View File

@@ -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",

View File

@@ -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`;
});

View File

@@ -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) {

View File

@@ -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
};

View File

@@ -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

View File

@@ -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: {

View 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>
)}
</>
);
}

View File

@@ -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:

View 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
}
]
}
]
}
];

View File

@@ -41,7 +41,8 @@ export type SectionKeys =
| "legal"
| "developer"
| "about"
| "inbox";
| "inbox"
| "circle";
export type SectionGroupKeys =
| "account"

View File

@@ -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
*/

View File

@@ -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();

View File

@@ -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({

View File

@@ -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 () => {

View File

@@ -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({

View File

@@ -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 |

View File

@@ -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": {

View 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!

View File

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

View File

@@ -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>;

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

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;
}
}
}

View File

@@ -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;
};
};

View File

@@ -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";

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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,

View File

@@ -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>;

View File

@@ -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",

View File

@@ -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" />
<!--

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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;
}
};

View File

@@ -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 isnt 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"
// })
// })
// ];
// }
});

View File

@@ -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());
}
}
}

View File

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

View File

@@ -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}

View File

@@ -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>"`;

View File

@@ -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");
});

View File

@@ -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;
}

View File

@@ -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";
}

View File

@@ -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",
}
`;

View 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();
});
});

View File

@@ -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;
}
},
{

View File

@@ -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");

View File

@@ -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",
}
`;

View File

@@ -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();
});
});

View File

@@ -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(

View File

@@ -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