Compare commits

...

13 Commits

Author SHA1 Message Date
Ammar Ahmed
747a2e2bf4 mobile: release 3.3.10-beta.6 2025-12-12 11:47:58 +05:00
Abdullah Atta
57e9cd3e6a web: update lockfile 2025-12-12 11:46:47 +05:00
Ammar Ahmed
0ec83fd389 mobile: fix wrapped ui on tablets 2025-12-12 11:46:47 +05:00
Abdullah Atta
d23661c0b8 global: update package lockfiles 2025-12-12 11:46:47 +05:00
Abdullah Atta
11200b5c3a web: bump version to 3.3.6-beta.3 2025-12-12 11:46:47 +05:00
01zulfi
c4acb23164 editor: fix callout collapse/expand on clicking right after its heading
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-12 11:46:47 +05:00
Abdullah Atta
c1d9427e64 config: disable rebase when sync vscode setting 2025-12-12 11:46:47 +05:00
Ammar Ahmed
e611261a07 mobile: wrapped 2025 2025-12-12 11:46:47 +05:00
01zulfi
7ccfba67e5 web: wrapped 2025
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: refined wrapped ui

web: make wrapped work automatically for future years

web: fix emoji for colors

web: format word count
2025-12-12 11:46:47 +05:00
01zulfi
122df1bb35 web: allow closing file drag overlay by click or esc key (#9044)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-12 11:46:47 +05:00
01zulfi
73e038540a editor: add shortcut to open search and replace (#9043)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-12 11:46:47 +05:00
01zulfi
5be7c7f456 editor: hide horizontal rule if its under a collapsed heading (#9038)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-12-12 11:46:46 +05:00
Ammar Ahmed
ae27b04fdb mobile: keep showing custom message on card when there are announcements 2025-12-10 10:39:00 +05:00
35 changed files with 4206 additions and 591 deletions

View File

@@ -11,5 +11,5 @@
"cache": true,
"cacheStrategy": "content"
},
"git.rebaseWhenSync": true
"git.rebaseWhenSync": false
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.6-beta.2",
"version": "3.3.6-beta.3",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",

View File

@@ -125,7 +125,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled true
versionCode 3084
versionCode 3085
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -324,7 +324,9 @@ export const Signup = ({
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: DefaultAppStyles.GAP,
width: DDS.isTab ? "50%" : "100%",
alignSelf: "center"
}}
>
<Paragraph

View File

@@ -43,7 +43,7 @@ export const Card = ({
const fontScale = Dimensions.get("window").fontScale;
return !messageBoardState.visible ||
(announcements && announcements.length) ? null : (
(announcements && announcements.length && !customMessage) ? null : (
<View
style={{
width: "100%",

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SubscriptionPlan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React from "react";
import { FlatList, View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
@@ -140,19 +141,35 @@ export function SideMenuHome() {
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{(subscriptionType === SubscriptionPlan.FREE ||
!subscriptionType ||
!user) &&
!SettingsService.getProperty("serverUrls") ? (
{dayjs().month() !== 11 ? (
<>
{(subscriptionType === SubscriptionPlan.FREE ||
!subscriptionType ||
!user) &&
!SettingsService.getProperty("serverUrls") ? (
<Button
title={pro.title}
style={{
width: "100%"
}}
type="accent"
onPress={pro.onPress}
/>
) : null}
</>
) : (
<Button
title={pro.title}
title={`Wrapped ${dayjs().year()} 🎉`}
style={{
width: "100%"
}}
type="accent"
onPress={pro.onPress}
bold
type="secondaryAccented"
onPress={() => {
Navigation.navigate("Wrapped");
}}
/>
) : null}
)}
</View>
</View>
);

View File

@@ -273,6 +273,7 @@ let Settings: any = null;
let ManageTags: any = null;
let AddReminder: any = null;
let PayWall: any = null;
let Wrapped: any = null;
export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
@@ -384,6 +385,14 @@ export const RootNavigation = () => {
return PayWall;
}}
/>
<RootStack.Screen
name="Wrapped"
getComponent={() => {
Wrapped = Wrapped || require("../screens/wrapped").default;
return Wrapped;
}}
/>
</RootStack.Navigator>
</NavigationContainer>
);

File diff suppressed because it is too large Load Diff

View File

@@ -71,7 +71,8 @@ const routeNames = {
Archive: "Archive",
ManageTags: "ManageTags",
AddReminder: "AddReminder",
PayWall: "PayWall"
PayWall: "PayWall",
Wrapped: "Wrapped"
};
export type NavigationProps<T extends RouteName> = NativeStackScreenProps<

View File

@@ -112,6 +112,7 @@ export interface RouteParams extends ParamListBase {
context: "signup" | "logged-in" | "logged-out" | "subscribed";
state?: BillingState;
};
Wrapped: GenericRouteParam;
}
export type RouteName = keyof RouteParams;

View File

@@ -1029,7 +1029,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2164;
CURRENT_PROJECT_VERSION = 2165;
DEVELOPMENT_TEAM = 53CWBG3QUC;
ENABLE_BITCODE = NO;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1104,7 +1104,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "3.3.10";
MARKETING_VERSION = 3.3.10;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1135,7 +1135,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 2164;
CURRENT_PROJECT_VERSION = 2165;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1210,7 +1210,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "3.3.10";
MARKETING_VERSION = 3.3.10;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1367,7 +1367,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2164;
CURRENT_PROJECT_VERSION = 2165;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1379,7 +1379,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "3.3.10";
MARKETING_VERSION = 3.3.10;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1410,7 +1410,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2164;
CURRENT_PROJECT_VERSION = 2165;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "3.3.10";
MARKETING_VERSION = 3.3.10;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1453,7 +1453,7 @@
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2164;
CURRENT_PROJECT_VERSION = 2165;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 53CWBG3QUC;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -1534,7 +1534,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = "3.3.10";
MARKETING_VERSION = 3.3.10;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1565,7 +1565,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2164;
CURRENT_PROJECT_VERSION = 2165;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
@@ -1647,7 +1647,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = "3.3.10";
MARKETING_VERSION = 3.3.10;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -2246,6 +2246,34 @@ PODS:
- Yoga
- react-native-upload (6.28.0):
- React
- react-native-view-shot (4.0.3):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-webview (13.16.0):
- boost
- DoubleConversion
@@ -3465,6 +3493,7 @@ DEPENDENCIES:
- "react-native-sodium (from `../node_modules/@ammarahmed/react-native-sodium`)"
- react-native-theme-switch-animation (from `../node_modules/react-native-theme-switch-animation`)
- "react-native-upload (from `../node_modules/@ammarahmed/react-native-upload`)"
- react-native-view-shot (from `../node_modules/react-native-view-shot`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
@@ -3680,6 +3709,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-theme-switch-animation"
react-native-upload:
:path: "../node_modules/@ammarahmed/react-native-upload"
react-native-view-shot:
:path: "../node_modules/react-native-view-shot"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-NativeModulesApple:
@@ -3881,6 +3912,7 @@ SPEC CHECKSUMS:
react-native-sodium: 066f76e46c9be13e9260521e3fa994937c4cdab4
react-native-theme-switch-animation: 449d6db7a760f55740505e7403ae8061debc9a7e
react-native-upload: ddf12a152c62fcafa202ef0404d3d46333a6a6a6
react-native-view-shot: 6c008e58f4720de58370848201c5d4a082c6d4ca
react-native-webview: 654f794a7686b47491cf43aa67f7f428bea00eed
React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.3.10-beta.4",
"version": "3.3.10-beta.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.3.10-beta.4",
"version": "3.3.10-beta.5",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -123,6 +123,7 @@
"react-native-tooltips": "^1.0.3",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "10.3.0",
"react-native-view-shot": "^4.0.3",
"react-native-webview": "^13.13.5",
"react-native-wheel-color-picker": "^1.3.1",
"react-native-worklets": "^0.7.1",
@@ -228,6 +229,7 @@
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.9-alpha",
"@types/mime-db": "^1.43.5",
"alfaaz": "^1.1.0",
"async-mutex": "0.5.0",
"dayjs": "1.11.13",
"dom-serializer": "^2.0.0",
@@ -7498,6 +7500,15 @@
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8554,6 +8565,15 @@
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -11634,6 +11654,19 @@
"entities": "^4.4.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/htmlparser2": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz",
@@ -18066,6 +18099,19 @@
"node": ">=10"
}
},
"node_modules/react-native-view-shot": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-4.0.3.tgz",
"integrity": "sha512-USNjYmED7C0me02c1DxKA0074Hw+y/nxo+xJKlffMvfUWWzL5ELh/TJA/pTnVqFurIrzthZDPtDM7aBFJuhrHQ==",
"license": "MIT",
"dependencies": {
"html2canvas": "^1.4.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-webview": {
"version": "13.16.0",
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.0.tgz",
@@ -20279,6 +20325,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/thingies": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz",
@@ -21090,6 +21145,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.3.10-beta.5",
"version": "3.3.10-beta.6",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {
@@ -139,6 +139,7 @@
"react-native-tooltips": "^1.0.3",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "10.3.0",
"react-native-view-shot": "^4.0.3",
"react-native-webview": "^13.13.5",
"react-native-wheel-color-picker": "^1.3.1",
"react-native-worklets": "^0.7.1",

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.6-beta.2",
"version": "3.3.6-beta.3",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",
@@ -57,6 +57,7 @@
"file-saver": "^2.0.5",
"hash-wasm": "4.12.0",
"hotkeys-js": "^3.8.3",
"htmlparser2": "^10.0.0",
"katex": "0.16.11",
"mac-scrollbar": "0.13.6",
"mutative": "^1.1.0",

View File

@@ -48,6 +48,7 @@ import { getFontSizes } from "@notesnook/theme/theme/font/fontsize.js";
import { useWindowControls } from "./hooks/use-window-controls";
import { STATUS_BAR_HEIGHT } from "./common/constants";
import { NavigationEvents } from "./navigation";
import { db } from "./common/db";
new WebExtensionRelay();
@@ -143,6 +144,10 @@ function DesktopAppContents() {
const isTablet = useTablet();
const navPane = useRef<SplitPaneImperativeHandle>(null);
useEffect(() => {
(async () => {})();
}, []);
useEffect(() => {
if (isTablet) navPane.current?.collapse(0);
else if (navPane.current?.isCollapsed(0)) navPane.current?.expand(0);

View File

@@ -25,6 +25,7 @@ import {
isFeatureSupported
} from "./utils/feature-check";
import { initializeLogger } from "./utils/logger";
import { shouldShowWrapped } from "./utils/should-show-wrapped";
type Route<TProps = null> = {
component: () => Promise<{
@@ -55,6 +56,9 @@ const routes = {
"/plans": {
component: () => import("./views/plans")
},
"/wrapped": {
component: () => import("./views/wrapped")
},
"/checkout": {
component: () => import("./views/checkout")
},
@@ -122,6 +126,12 @@ function getRoute(): RouteWithPath<AuthProps> | RouteWithPath {
routes[path] ? { route: routes[path], path } : null
) as RouteWithPath<AuthProps> | null;
if (route?.path === "/wrapped" && !shouldShowWrapped())
return {
route: routes.default,
path: "default"
};
return signup || sessionExpired || route || fallback;
}

View File

@@ -850,17 +850,27 @@ function useDragOverlay() {
e.preventDefault();
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
hideOverlay();
}
}
dropElement.addEventListener("dragenter", showOverlay);
overlay.addEventListener("drop", hideOverlay);
overlay.addEventListener("dragenter", allowDrag);
overlay.addEventListener("dragover", allowDrag);
overlay.addEventListener("dragleave", hideOverlay);
overlay.addEventListener("click", hideOverlay);
document.addEventListener("keydown", handleKeyDown);
return () => {
dropElement.removeEventListener("dragenter", showOverlay);
overlay.removeEventListener("drop", hideOverlay);
overlay.removeEventListener("dragenter", allowDrag);
overlay.removeEventListener("dragover", allowDrag);
overlay.removeEventListener("dragleave", hideOverlay);
overlay.removeEventListener("click", hideOverlay);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);

View File

@@ -0,0 +1,143 @@
/*
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 { Box, Flex, Text } from "@theme-ui/components";
import { SxProp } from "@theme-ui/core";
type MonthlyActivityHeatmapProps = {
monthlyStats: Record<string, number>;
year?: number;
} & SxProp;
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
const MONTHS_SHORT = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
];
export function MonthlyActivityHeatmap({
monthlyStats
}: MonthlyActivityHeatmapProps) {
// Find max count for bar height calculation
const maxCount = Math.max(...Object.values(monthlyStats), 1);
// Create month data with counts
const monthData = MONTHS.map((month) => ({
month,
count: monthlyStats[month] || 0
}));
return (
<>
{/* Bar chart */}
<Flex
sx={{
alignItems: "flex-end",
justifyContent: "space-between",
gap: 2,
height: "100%",
width: "100%",
position: "relative",
pb: 2
}}
>
{monthData.map((data, index) => {
const barHeight = maxCount > 0 ? (data.count / maxCount) * 100 : 0;
return (
<Flex
key={data.month}
sx={{
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-end",
flex: 1,
gap: 2,
cursor: "pointer",
"&:hover .tooltip": { opacity: 1 },
"&:hover .bar": { bg: "accent" },
height: "100%"
}}
>
<Flex
className="tooltip"
sx={{ opacity: 0, bg: "background", zIndex: 1000 }}
>
<Text variant="body" sx={{ fontWeight: "bold" }}>
{data.count}
</Text>
</Flex>
<Box
className="bar"
sx={{
width: "100%",
height: `${barHeight}%`,
minHeight: data.count > 0 ? "10px" : "5px",
bg: maxCount === data.count ? "accent" : "paragraph",
borderRadius: "4px 4px 0 0",
transition: "all 0.3s ease",
position: "relative",
"&:hover": {
transform: "scaleY(1.05)",
transformOrigin: "bottom"
}
}}
></Box>
<Text
sx={{
fontSize: "11px",
color: "paragraph-secondary",
fontWeight: "normal",
transition: "all 0.3s ease"
}}
>
{MONTHS_SHORT[index]}
</Text>
</Flex>
);
})}
</Flex>
</>
);
}

View File

@@ -97,7 +97,6 @@ import { strings } from "@notesnook/intl";
import Tags from "../../views/tags";
import { Notebooks } from "../../views/notebooks";
import { UserProfile } from "../../dialogs/settings/components/user-profile";
import { SUBSCRIPTION_STATUS } from "../../common/constants";
import {
checkFeature,
createSetDefaultHomepageMenuItem,
@@ -105,7 +104,6 @@ import {
withFeatureCheck
} from "../../common";
import { TabItem } from "./tab-item";
import Notice from "../notice";
import { Freeze } from "react-freeze";
import { CREATE_BUTTON_MAP } from "../../common";
import { useStore as useNotebookStore } from "../../stores/notebook-store";
@@ -118,6 +116,7 @@ import {
useIsFeatureAvailable
} from "@notesnook/common";
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
import { shouldShowWrapped } from "../../utils/should-show-wrapped";
type Route = {
id: "notes" | "favorites" | "reminders" | "monographs" | "trash" | "archive";
@@ -449,7 +448,17 @@ function NavigationMenu({ onExpand }: { onExpand?: () => void }) {
</FlexScrollContainer>
</Freeze>
</Flex>
{currentTab.id === "home" && !isCollapsed ? <Notice /> : null}
{currentTab.id === "home" && !isCollapsed && shouldShowWrapped() ? (
<Button
variant="accent"
sx={{ m: 2 }}
onClick={() => {
hardNavigate("/wrapped");
}}
>
🎉 Wrapped {new Date().getFullYear()}
</Button>
) : null}
</ScopedThemeProvider>
);
}

View File

@@ -0,0 +1,23 @@
/*
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/>.
*/
export const shouldShowWrapped = () => {
const now = new Date();
return now.getMonth() === 11;
};

View File

@@ -0,0 +1,776 @@
/*
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 { Button, Flex, Text, Box, FlexProps } from "@theme-ui/components";
import { db } from "../common/db";
import { useState, useEffect, useRef } from "react";
import { NoteStats, WrappedStats } from "@notesnook/core";
import { formatBytes } from "@notesnook/common";
import { ArrowDown, ArrowLeft, Loading } from "../components/icons";
import { hardNavigate } from "../navigation";
import { MonthlyActivityHeatmap } from "../components/monthly-activity-heatmap";
function formatNumber(num: number) {
return num.toLocaleString();
}
function formatCount(num: number) {
return num > 1000 ? `${(num / 1000).toFixed(0)}k` : num.toString();
}
interface SlideProps {
children: React.ReactNode;
pattern?: "dots" | "grid" | "diagonal" | "none";
}
function Slide({
children,
pattern = "none",
sx,
...flexProps
}: SlideProps & FlexProps) {
const slideRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
},
{ threshold: 0.5 }
);
if (slideRef.current) {
observer.observe(slideRef.current);
}
return () => {
if (slideRef.current) {
observer.unobserve(slideRef.current);
}
};
}, []);
const getBackgroundPattern = () => {
switch (pattern) {
case "dots":
return "radial-gradient(circle, var(--border) 1px, transparent 1px)";
case "grid":
return "linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px)";
case "diagonal":
return "repeating-linear-gradient(-45deg, transparent, transparent 20px, var(--border) 20px, var(--border) 21px)";
default:
return "none";
}
};
const getBackgroundSize = () => {
switch (pattern) {
case "dots":
return "20px 20px";
case "grid":
return "30px 30px";
case "diagonal":
default:
return "auto";
}
};
return (
<Flex
ref={slideRef}
sx={{
minHeight: "100vh",
minWidth: "100%",
scrollSnapAlign: "start",
scrollSnapStop: "always",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
p: 4,
position: "relative",
backgroundImage: getBackgroundPattern(),
backgroundSize: getBackgroundSize(),
backgroundPosition: "center"
}}
>
<Flex
{...flexProps}
sx={{
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
opacity: isVisible ? 1 : 0,
transform: isVisible ? "translateY(0)" : "translateY(50px)",
transition: "opacity 0.8s ease-out, transform 0.8s ease-out",
...sx
}}
>
{children}
</Flex>
</Flex>
);
}
function WelcomeSlide({ loading }: { loading: boolean }) {
return (
<Slide pattern="dots">
<Text
sx={{
fontSize: "6rem",
fontWeight: "bold",
mb: 3,
textAlign: "center",
background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animation: "slideUp 1s ease-out"
}}
>
🎉
</Text>
<Text
variant="heading"
sx={{
fontSize: ["2rem", "3rem", "4rem"],
fontWeight: "bold",
mb: 2,
textAlign: "center",
animation: "slideUp 1s ease-out 0.2s both"
}}
>
Your {new Date().getFullYear()} Wrapped
</Text>
{loading ? (
<Loading
sx={{
animation: "fadeIn 1s ease-out 0.4s both"
}}
size={30}
/>
) : (
<>
<Text
sx={{
fontSize: ["1rem", "1.2rem"],
textAlign: "center",
animation: "fadeIn 1s ease-out 0.4s both"
}}
>
Let&apos;s look back at your year in Notesnook
</Text>
<Text
variant="body"
sx={{
mt: 4,
fontSize: "title",
color: "paragraph-secondary",
textAlign: "center",
animation: "fadeIn 1s ease-out 0.4s both"
}}
>
Scroll down to explore
</Text>
<ArrowDown sx={{ mt: 2 }} color="paragraph-secondary" />
</>
)}
</Slide>
);
}
function TotalNotesSlide({ count }: { count: number }) {
return (
<Slide pattern="dots">
<Flex>
<Flex sx={{ flexDirection: "column" }}>
<Text
variant="body"
sx={{
fontSize: ["1.2rem", "1.5rem"],
textAlign: "center",
animation: "fadeIn 0.8s ease-out"
}}
>
You created
</Text>
<Text
variant="heading"
sx={{
fontSize: ["5rem", "7rem", "8rem"],
textAlign: "center",
color: "accent",
animation: "scaleIn 0.8s ease-out 0.2s both"
}}
>
{formatNumber(count)}
</Text>
<Text
variant="body"
sx={{
fontSize: ["1.2rem", "1.5rem"],
textAlign: "center",
animation: "fadeIn 0.8s ease-out 0.4s both"
}}
>
notes this year
</Text>
</Flex>
<Flex
sx={{
flexDirection: "column",
ml: [0, 5],
mt: [4, 0],
borderLeft: ["none", "1px solid var(--border)"],
pl: [0, 5],
borderTop: ["1px solid var(--border)", "none"]
}}
>
<Text
variant="body"
color="paragraph-secondary"
sx={{
fontSize: ["1.2rem", "1.2rem"],
animation: "fadeIn 0.8s ease-out",
lineHeight: 1.8
}}
>
That&apos;s <strong>{formatNumber(count)}</strong>
<br />
ideas
<br />
thoughts
<br />
memories.
<br />
100% encrypted.
<br />
100% yours.
</Text>
</Flex>
</Flex>
</Slide>
);
}
const TOTAL_WORDS_TAGLINES = {
1000: "That's longer than the average blog post on Medium!",
10000: "That's real commitment to your mindspace.",
25000: "That's almost the length of a short novel!",
50000:
"That's the length of The Great Gatsby if you were F. Scott Fitzgerald.",
100000: "You wrote more than many published authors this year.",
250000: "Woah! Your notes could fill a small library.",
500000: "Your vault is becoming a chronicle.",
1000000: "That's longer than the entire Harry Potter series!"
};
function TotalWordsSlide({ count }: { count: number }) {
const tagline = Object.entries(TOTAL_WORDS_TAGLINES)
.sort((a, b) => Number(b[0]) - Number(a[0]))
.find(([threshold]) => count >= Number(threshold))?.[1];
return (
<Slide pattern="dots">
<Text
variant="body"
sx={{
fontSize: ["1.2rem", "1.5rem"],
textAlign: "center",
animation: "fadeIn 0.8s ease-out"
}}
>
You wrote a total of
</Text>
<Text
variant="heading"
sx={{
fontSize: ["5rem", "7rem"],
textAlign: "center",
color: "accent",
animation: "scaleIn 0.8s ease-out 0.2s both"
}}
>
{formatNumber(count)}
</Text>
<Text
variant="body"
sx={{
fontSize: ["1.2rem", "1.5rem"],
textAlign: "center",
animation: "fadeIn 0.8s ease-out 0.4s both"
}}
>
words this year
</Text>
{tagline ? (
<Text
variant="body"
color="paragraph-secondary"
sx={{
fontSize: ["1.2rem", "1.2rem"],
animation: "fadeIn 0.8s ease-out",
borderTop: "1px solid var(--border)",
mt: 3,
pt: 3
}}
>
{tagline}
</Text>
) : null}
</Slide>
);
}
type ActivityStatsSlideProps = {
mostNotesCreatedInMonth: NoteStats["mostNotesCreatedInMonth"];
mostNotesCreatedInDay: NoteStats["mostNotesCreatedInDay"];
};
function ActivityStatsSlide({
mostNotesCreatedInMonth,
mostNotesCreatedInDay
}: ActivityStatsSlideProps) {
if (!mostNotesCreatedInMonth && !mostNotesCreatedInDay) return null;
return (
<Slide pattern="diagonal" sx={{ alignItems: "start" }}>
{mostNotesCreatedInMonth && (
<>
<Text
variant="body"
sx={{
fontSize: ["1.2rem", "1.5rem"]
}}
>
Your most productive month was
</Text>
<Flex
sx={{
alignItems: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<Text
variant="heading"
sx={{
fontSize: ["5rem", "7rem", "3rem"]
}}
>
{mostNotesCreatedInMonth.month}
</Text>
<Text
variant="body"
sx={{ fontSize: "1rem" }}
color="paragraph-secondary"
>
{formatNumber(mostNotesCreatedInMonth.count)} notes
</Text>
</Flex>
</>
)}
{mostNotesCreatedInDay && (
<>
<Text
variant="body"
sx={{
fontSize: ["1.2rem", "1.5rem"],
mt: 5
}}
>
Your favorite day to write was
</Text>
<Flex
sx={{
alignItems: "center",
justifyContent: "space-between",
width: "100%"
}}
>
<Text
variant="heading"
sx={{
fontSize: ["5rem", "7rem", "3rem"]
}}
>
{mostNotesCreatedInDay.day}
</Text>
<Text
variant="body"
sx={{ fontSize: "1rem" }}
color="paragraph-secondary"
>
{formatNumber(mostNotesCreatedInDay.count)} notes
</Text>
</Flex>
</>
)}
</Slide>
);
}
function SummarySlide({ stats }: { stats: WrappedStats }) {
return (
<Slide
pattern="dots"
sx={{
bg: "background-secondary",
border: "1px solid var(--accent)",
borderRadius: "dialog",
boxShadow: "-15px 15px 0px 0px var(--accent)",
p: 5
}}
>
<svg
style={{
height: 20,
width: 20,
position: "absolute",
bottom: "20px",
right: "20px"
}}
>
<use href="#themed-logo" />
</svg>
<Text
variant="heading"
sx={{
fontSize: "1.2rem",
fontWeight: 800,
textAlign: "center",
transform: "skew(-10deg) scaleX(1.5)",
letterSpacing: "-1px",
mb: "25px",
textDecorationLine: "underline",
textDecorationColor: "border"
}}
>
NOTESNOOK WRAPPED {new Date().getFullYear()}
</Text>
<Flex
sx={{
flexDirection: ["column", "row"],
gap: "30px",
justifyContent: "center",
alignItems: "center"
}}
>
<Flex
sx={{
flexDirection: "column",
flex: 1
}}
>
<Box
sx={{
display: "grid",
gridTemplateColumns: "1fr 1fr 1fr",
gridTemplateRows: "1fr 1fr",
gap: 4,
height: "100%"
}}
>
{[
{
icon: "📝",
count: stats.totalNotes,
label: "Notes"
},
{
icon: "🎨",
count: stats.totalColors,
label: "Colors"
},
{
icon: "📚",
count: stats.totalNotebooks,
label: "Notebooks"
},
{
icon: "🏷️",
count: stats.totalTags,
label: "Tags"
},
{
icon: "📂",
count: stats.totalAttachments,
label: "Files"
},
{
icon: "☁️",
count: stats.totalMonographs,
label: "Monographs"
}
].map(({ icon, count, label }) => (
<Flex
key={label}
sx={{
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 1
}}
>
<Text sx={{ fontSize: "1.5rem" }}>{icon}</Text>
<Text
sx={{
fontSize: "2rem",
fontWeight: "bold"
}}
>
{formatCount(count)}
</Text>
<Text sx={{ fontSize: "0.85rem", color: "fontTertiary" }}>
{label}
</Text>
</Flex>
))}
</Box>
<Flex
sx={{
flexDirection: "column",
gap: 2,
borderTop: "1px solid var(--border)",
mt: 6,
pt: 3,
"& strong": {
color: "accent"
}
}}
>
<Text sx={{ fontSize: "1rem", fontWeight: "bold" }}>
Fun facts of the year
</Text>
{stats.mostNotesCreatedInMonth && (
<Text sx={{ fontSize: "0.9rem", color: "fontTertiary" }}>
📅 Your most productive month was{" "}
<Text as="strong">{stats.mostNotesCreatedInMonth.month}</Text>
</Text>
)}
{stats.mostNotesCreatedInDay && (
<Text sx={{ fontSize: "0.9rem", color: "fontTertiary" }}>
🗓 Your favorite day to write was{" "}
<Text as="strong">{stats.mostNotesCreatedInDay.day}</Text>
</Text>
)}
{stats.largestNote && (
<Text
sx={{
fontSize: "0.9rem",
color: "fontTertiary",
maxWidth: 340
}}
>
📝 Your longest note was{" "}
<Text as="strong">
{formatNumber(stats.largestNote.length)}
{" words"}
</Text>
</Text>
)}
{stats.largestAttachment && (
<Text
sx={{
fontSize: "0.9rem",
color: "fontTertiary",
maxWidth: 340
}}
>
🔗 Your largest attachment was{" "}
<strong>{formatBytes(stats.largestAttachment.size)}</strong>
</Text>
)}
</Flex>
</Flex>
<Flex
sx={{
flexDirection: "column",
justifyContent: "stretch",
height: "100%",
flex: 1
}}
>
<Flex
sx={{
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}
>
<Text sx={{ fontSize: "2.5rem" }}></Text>
<Text
sx={{
fontSize: "3rem",
fontWeight: "bold",
color: "accent"
}}
>
{formatNumber(stats.totalWords)}
</Text>
<Text sx={{ fontSize: "1rem", color: "fontTertiary" }}>
Words Written
</Text>
</Flex>
<Flex
sx={{
flexDirection: "column",
borderTop: "1px solid var(--border)",
mt: 6,
pt: 2,
flex: 1
}}
>
<MonthlyActivityHeatmap monthlyStats={stats.monthlyStats} />
<Text
sx={{
fontSize: "1rem",
color: "fontTertiary",
textAlign: "center"
}}
>
Notes per month
</Text>
</Flex>
</Flex>
</Flex>
<Text
variant="body"
color="paragraph-secondary"
sx={{ mt: 2, borderTop: "1px solid var(--border)", pt: 2 }}
>
Generated 100% locally on your device.
</Text>
</Slide>
);
}
export default function Wrapped() {
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState<WrappedStats>();
useEffect(() => {
async function loadWrapped() {
setLoading(true);
try {
console.time("wrapped - getting from core");
const wrapped = await db.wrapped.get();
console.timeEnd("wrapped - getting from core");
setStats(wrapped);
} finally {
setLoading(false);
}
}
loadWrapped();
}, []);
return (
<>
<style>
{`
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
`}
</style>
<Flex
sx={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
flexDirection: "column",
overflowY: "scroll",
overflowX: "hidden",
scrollSnapType: "y mandatory",
scrollBehavior: "smooth",
"&::-webkit-scrollbar": {
display: "none"
},
msOverflowStyle: "none",
scrollbarWidth: "none"
}}
>
<WelcomeSlide loading={loading} />
{stats ? (
<>
<TotalNotesSlide count={stats.totalNotes} />
{stats.totalWords > 0 && (
<TotalWordsSlide count={stats.totalWords} />
)}
{(stats.mostNotesCreatedInMonth || stats.mostNotesCreatedInDay) && (
<ActivityStatsSlide
mostNotesCreatedInMonth={stats.mostNotesCreatedInMonth}
mostNotesCreatedInDay={stats.mostNotesCreatedInDay}
/>
)}
<SummarySlide stats={stats} />
</>
) : null}
</Flex>
<Button
onClick={() => hardNavigate("/")}
variant="secondary"
sx={{
position: "fixed",
top: 3,
left: 3,
zIndex: 1000
}}
>
<Flex sx={{ alignItems: "center", gap: 1, justifyContent: "center" }}>
<ArrowLeft size={16} />
<Text variant="body">Go back to app</Text>
</Flex>
</Button>
</>
);
}

View File

@@ -67,6 +67,7 @@ The following keyboard shortcuts will help you navigate Notesnook faster.
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
| Open search | Ctrl F | Ctrl F | ⌘ F |
| Open search and replace | Ctrl Alt F | Ctrl Alt F | ⌘ ⌥ F |
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |

View File

@@ -41,6 +41,7 @@
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.9-alpha",
"@types/mime-db": "^1.43.5",
"alfaaz": "^1.1.0",
"async-mutex": "0.5.0",
"dayjs": "1.11.13",
"dom-serializer": "^2.0.0",

View File

@@ -335,6 +335,12 @@ export const tiptapKeys = {
category: "Editor",
type: "tiptap"
},
openSearchAndReplace: {
keys: "Mod-Alt-f",
description: "Open search and replace",
category: "Editor",
type: "tiptap"
},
toggleStrike: {
keys: "Mod-Shift-S",
description: "Toggle strike",

View File

@@ -18,6 +18,7 @@
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.9-alpha",
"@types/mime-db": "^1.43.5",
"alfaaz": "^1.1.0",
"async-mutex": "0.5.0",
"dayjs": "1.11.13",
"dom-serializer": "^2.0.0",
@@ -1391,6 +1392,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/alfaaz": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/alfaaz/-/alfaaz-1.1.0.tgz",
"integrity": "sha512-J/P07R41APslK7NmD5303bwStN8jpRA4DdvtLeAr1Jhfj6XWGrASUWI0G6jbWjJAZyw3Lu1Pb4J8rsM/cb+xDQ==",
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",

View File

@@ -73,6 +73,7 @@
"@streetwriters/kysely": "^0.27.4",
"@streetwriters/showdown": "^3.0.9-alpha",
"@types/mime-db": "^1.43.5",
"alfaaz": "^1.1.0",
"async-mutex": "0.5.0",
"dayjs": "1.11.13",
"dom-serializer": "^2.0.0",

View File

@@ -83,6 +83,7 @@ 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";
import { Wrapped } from "./wrapped.js";
type EventSourceConstructor = new (
uri: string,
@@ -224,6 +225,8 @@ class Database {
inboxApiKeys = new InboxApiKeys(this, this.tokenManager);
wrapped = new Wrapped(this);
/**
* @deprecated only kept here for migration purposes
*/

View File

@@ -0,0 +1,342 @@
/*
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 { Parser } from "htmlparser2";
import Database from "./index.js";
import { countWords } from "alfaaz";
import { DatabaseSchema, isFalse } from "../database/index.js";
import { SelectQueryBuilder } from "@streetwriters/kysely";
const dayNames = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
];
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
];
export type NoteStats = {
totalNotes: number;
totalWords: number;
totalMonographs: number;
mostNotesCreatedInMonth: { month: string; count: number } | null;
mostNotesCreatedInDay: { day: string; count: number } | null;
monthlyStats: Record<string, number>;
dayOfWeekStats: Record<string, number>;
largestNote: { title: string; length: number } | null;
};
export type OrganizationStats = {
totalNotebooks: number;
totalTags: number;
mostUsedTags: { id: string; title: string; noteCount: number }[];
mostActiveNotebooks: { id: string; title: string; noteCount: number }[];
totalColors: number;
};
export type AttachmentStats = {
totalAttachments: number;
totalStorageUsed: number;
largestAttachment: { id: string; filename: string; size: number } | null;
mostCommonFileType: string | null;
};
export type WrappedStats = NoteStats & OrganizationStats & AttachmentStats;
export class Wrapped {
constructor(private readonly db: Database) {}
async get(): Promise<WrappedStats> {
const { startDate, endDate } = this.getYearRange();
const [noteStats, organizationStats, attachmentStats] = await Promise.all([
this.getNoteStats(startDate, endDate),
this.getOrganizationStats(startDate, endDate),
this.getAttachmentStats(startDate, endDate)
]);
return {
...noteStats,
...organizationStats,
...attachmentStats
};
}
private getYearRange(): { startDate: number; endDate: number } {
const year = new Date().getFullYear();
const startDate = new Date(year, 0, 1, 0, 0, 0, 0).getTime();
const endDate = new Date(year + 1, 0, 1, 0, 0, 0, 0).getTime();
return { startDate, endDate };
}
private async getNoteStats(
startDate: number,
endDate: number
): Promise<NoteStats> {
const notesSelector = this.db
.sql()
.selectFrom("notes")
.where((eb) => eb("dateCreated", ">=", startDate))
.where((eb) => eb("dateCreated", "<", endDate))
.where(isFalse("deleted"))
.where(isFalse("dateDeleted"));
const notes = await notesSelector.select(["notes.dateCreated"]).execute();
const monthlyStats: Map<string, number> = new Map();
const dayOfWeekStats: Map<string, number> = new Map();
let totalNotes = 0;
for (const note of notes) {
if (!note.dateCreated) continue;
totalNotes++;
const month = new Date(note.dateCreated).getMonth();
const monthName = monthNames[month];
monthlyStats.set(monthName, (monthlyStats.get(monthName) || 0) + 1);
const dayOfWeek = new Date(note.dateCreated).getDay();
const dayName = dayNames[dayOfWeek];
dayOfWeekStats.set(dayName, (dayOfWeekStats.get(dayName) || 0) + 1);
}
let mostNotesCreatedInMonth: NoteStats["mostNotesCreatedInMonth"] = null;
for (const [month, count] of monthlyStats.entries()) {
if (!mostNotesCreatedInMonth || count > mostNotesCreatedInMonth.count) {
mostNotesCreatedInMonth = { month, count };
}
}
let mostNotesCreatedInDay: NoteStats["mostNotesCreatedInDay"] = null;
let maxDayCount = 0;
for (const [day, count] of dayOfWeekStats.entries()) {
if (count > maxDayCount) {
maxDayCount = count;
mostNotesCreatedInDay = { day, count };
}
}
const totalMonographs = await this.db.monographs.all
.where((eb) =>
eb.and([
eb("dateCreated", ">=", startDate),
eb("dateCreated", "<", endDate)
])
)
.count();
const { largestNote, totalWords } = await this.countTotalWords(
notesSelector
);
return {
totalNotes,
totalWords,
totalMonographs,
largestNote: largestNote
? {
title: (await this.db.notes.note(largestNote.id))?.title || "",
length: largestNote.wordCount
}
: null,
monthlyStats: Object.fromEntries(monthlyStats),
dayOfWeekStats: Object.fromEntries(dayOfWeekStats),
mostNotesCreatedInMonth,
mostNotesCreatedInDay
};
}
private async countItemNotes<T extends { id: string; title: string }>(
items: T[],
itemType: "tag" | "notebook"
): Promise<Array<T & { noteCount: number }>> {
const allRelations = await this.db.relations
.from({ ids: items.map((item) => item.id), type: itemType }, "note")
.get();
const noteCounts: Map<string, number> = new Map();
for (const relation of allRelations) {
const itemId = relation.fromId;
noteCounts.set(itemId, (noteCounts.get(itemId) || 0) + 1);
}
return items
.map((item) => ({
...item,
noteCount: noteCounts.get(item.id) || 0
}))
.filter((item) => item.noteCount > 0)
.sort((a, b) => b.noteCount - a.noteCount);
}
private async getOrganizationStats(
startDate: number,
endDate: number
): Promise<OrganizationStats> {
const notebookSelector = this.db.notebooks.all
.where((eb) => eb("dateCreated", ">=", startDate))
.where((eb) => eb("dateCreated", "<", endDate));
const tagSelector = this.db.tags.all
.where((eb) => eb("dateCreated", ">=", startDate))
.where((eb) => eb("dateCreated", "<", endDate));
const [totalNotebooks, totalTags, tags, notebooks, totalColors] =
await Promise.all([
notebookSelector.count(),
tagSelector.count(),
tagSelector.fields(["tags.id", "tags.title"]).items(),
notebookSelector.fields(["notebooks.id", "notebooks.title"]).items(),
this.db.colors.all
.where((eb) => eb("dateCreated", ">=", startDate))
.where((eb) => eb("dateCreated", "<", endDate))
.count()
]);
const tagNotes = await this.countItemNotes(tags, "tag");
const mostUsedTags = tagNotes.slice(0, 3);
const notebookNotes = await this.countItemNotes(notebooks, "notebook");
const mostActiveNotebooks = notebookNotes.slice(0, 3);
return {
totalNotebooks,
totalTags,
mostUsedTags:
mostUsedTags.length > 0
? mostUsedTags
: tags.slice(0, 3).map((tag) => ({ ...tag, noteCount: 0 })),
mostActiveNotebooks:
mostActiveNotebooks.length > 0
? mostActiveNotebooks
: notebooks.slice(0, 3).map((n) => ({ ...n, noteCount: 0 })),
totalColors
};
}
private async getAttachmentStats(
startDate: number,
endDate: number
): Promise<AttachmentStats> {
const attachmentsSelector = this.db.attachments.all
.where((eb) => eb("dateCreated", ">=", startDate))
.where((eb) => eb("dateCreated", "<", endDate));
const totalAttachments = await attachmentsSelector.count();
if (totalAttachments === 0) {
return {
totalAttachments: 0,
totalStorageUsed: 0,
largestAttachment: null,
mostCommonFileType: null
};
}
const totalStorageUsed =
(await this.db.attachments.totalSize(attachmentsSelector)) || 0;
const attachments = await attachmentsSelector.items();
let largestAttachment: AttachmentStats["largestAttachment"] = null;
const mimeTypeCounts: Map<string, number> = new Map();
for (const attachment of attachments) {
if (!largestAttachment || attachment.size > largestAttachment.size) {
largestAttachment = {
id: attachment.id,
filename: attachment.filename,
size: attachment.size
};
}
const mimeType = attachment.mimeType.split("/")[0] || attachment.mimeType;
mimeTypeCounts.set(mimeType, (mimeTypeCounts.get(mimeType) || 0) + 1);
}
let mostCommonFileType: string | null = null;
let maxCount = 0;
for (const [mimeType, count] of mimeTypeCounts.entries()) {
if (count > maxCount) {
maxCount = count;
mostCommonFileType = mimeType;
}
}
return {
totalAttachments,
totalStorageUsed,
largestAttachment,
mostCommonFileType
};
}
private async countTotalWords(
selector: SelectQueryBuilder<DatabaseSchema, "notes", unknown>
) {
let words = 0;
let largestNote = { id: "", wordCount: 0 };
const contents = await this.db
.sql()
.selectFrom("content")
.where("noteId", "in", selector.select("id"))
.where(isFalse("locked"))
.where(isFalse("deleted"))
.select(["content.data", "content.noteId"])
.execute();
for (const content of contents) {
if (typeof content?.data !== "string") continue;
const counted = countWords(toTextContent(content.data));
words += counted;
if (content.noteId && counted > largestNote.wordCount) {
largestNote = { id: content.noteId, wordCount: counted };
}
}
return { totalWords: words, largestNote };
}
}
function toTextContent(html: string) {
let text = "";
const parser = new Parser({
ontext: (data) => {
text += data;
},
onclosetag() {
text += " ";
}
});
parser.write(html);
parser.end();
return text;
}

View File

@@ -45,3 +45,4 @@ export type { SyncOptions } from "./api/sync/index.js";
export { sanitizeTag } from "./collections/tags.js";
export { default as DataURL } from "./utils/dataurl.js";
export { type ResolveInternalLink } from "./content-types/tiptap.js";
export type * from "./api/wrapped.js";

View File

@@ -23,11 +23,9 @@ import {
textblockTypeInputRule
} from "@tiptap/core";
import { Heading as TiptapHeading } from "@tiptap/extension-heading";
import { isClickWithinBounds } from "../../utils/prosemirror.js";
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
import { Node } from "@tiptap/pm/model";
import { useToolbarStore } from "../../toolbar/stores/toolbar-store.js";
import { Decoration, DecorationSet } from "prosemirror-view";
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
import { Callout } from "../callout/callout.js";
const COLLAPSIBLE_BLOCK_TYPES = [
"paragraph",
@@ -44,7 +42,8 @@ const COLLAPSIBLE_BLOCK_TYPES = [
"outlineList",
"mathBlock",
"webclip",
"embed"
"embed",
"horizontalRule"
];
const HEADING_REGEX = /^(#{1,6})\s$/;
@@ -187,6 +186,17 @@ export const Heading = TiptapHeading.extend({
if (typeof getPos === "boolean") return;
const pos = getPos();
const resolvedPos = editor.state.doc.resolve(pos);
const callout = findParentNodeClosestToPos(
resolvedPos,
(node) => node.type.name === Callout.name
);
// the first callout heading's collapsibility is handled by callout itself
if (callout?.node.firstChild === node) {
return;
}
const clientX =
e instanceof MouseEvent ? e.clientX : e.touches[0].clientX;
const clientY =
@@ -194,7 +204,7 @@ export const Heading = TiptapHeading.extend({
const isRtl =
e.target.dir === "rtl" ||
findParentNodeClosestToPos(
editor.state.doc.resolve(pos),
resolvedPos,
(node) => !!node.attrs.textDirection
)?.node.attrs.textDirection === "rtl";

View File

@@ -33,7 +33,7 @@ type DispatchFn = (tr: Transaction) => void;
declare module "@tiptap/core" {
interface Commands<ReturnType> {
searchreplace: {
startSearch: () => ReturnType;
startSearch: (isReplacing?: boolean) => ReturnType;
endSearch: () => ReturnType;
search: (term: string, options?: SearchSettings) => ReturnType;
moveToNextResult: () => ReturnType;
@@ -51,7 +51,7 @@ interface Result {
interface SearchOptions {
searchResultClass: string;
onStartSearch: (term?: string) => boolean;
onStartSearch: (term?: string, isReplacing?: boolean) => boolean;
onEndSearch: () => boolean;
}
@@ -242,7 +242,7 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
addCommands() {
return {
startSearch:
() =>
(isReplacing) =>
({ state, commands }) => {
const term = !state.selection.empty
? state.doc.textBetween(
@@ -252,7 +252,7 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
: undefined;
if (term) commands.search(term);
return this.options.onStartSearch(term);
return this.options.onStartSearch(term, isReplacing);
},
endSearch:
() =>
@@ -357,6 +357,8 @@ export const SearchReplace = Extension.create<SearchOptions, SearchStorage>({
return {
[tiptapKeys.openSearch.keys]: ({ editor }) =>
editor.commands.startSearch(),
[tiptapKeys.openSearchAndReplace.keys]: ({ editor }) =>
editor.commands.startSearch(true),
Escape: ({ editor }) => editor.commands.endSearch()
};
},

View File

@@ -191,11 +191,12 @@ const useTiptap = (
extensions: [
...CoreExtensions,
SearchReplace.configure({
onStartSearch: (term) => {
onStartSearch: (term, isReplacing) => {
useEditorSearchStore.setState({
isSearching: true,
searchTerm: term,
focusNonce: Math.random()
focusNonce: Math.random(),
isReplacing: isReplacing
});
return true;
},