mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
147 Commits
fix-subscr
...
fix-loadin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9df5030c | ||
|
|
af014cf8c1 | ||
|
|
1ddd7b1e6e | ||
|
|
9ef0179566 | ||
|
|
38690d18d4 | ||
|
|
c07f5393f6 | ||
|
|
406c63721d | ||
|
|
97f3e02f59 | ||
|
|
5c6564f00b | ||
|
|
605e75efb3 | ||
|
|
496f9ba340 | ||
|
|
f98283f2dd | ||
|
|
2d32e85a64 | ||
|
|
1a56a22a33 | ||
|
|
c85204d0c1 | ||
|
|
1d8a958eaf | ||
|
|
b52f70d57d | ||
|
|
12687dae7b | ||
|
|
c53c3167d8 | ||
|
|
60df472d53 | ||
|
|
67fe25bc8b | ||
|
|
44a7aed308 | ||
|
|
f22937e1eb | ||
|
|
c816f8b15b | ||
|
|
fa4f4e7bea | ||
|
|
076cea6d6d | ||
|
|
d951b97bed | ||
|
|
bd888e57e5 | ||
|
|
3ba1234056 | ||
|
|
4c5a668114 | ||
|
|
06d1891389 | ||
|
|
c97a228ec8 | ||
|
|
d11a200ebb | ||
|
|
bfedc1f43e | ||
|
|
b148083750 | ||
|
|
cdfd7333cd | ||
|
|
2ed32efefb | ||
|
|
e3331e6dee | ||
|
|
b3406be1ad | ||
|
|
751d0ca861 | ||
|
|
a614693dfa | ||
|
|
c4bbceef67 | ||
|
|
d39e7c990d | ||
|
|
e248dbfa5b | ||
|
|
29f789493e | ||
|
|
2b0e5c41c8 | ||
|
|
e241b2fe72 | ||
|
|
a91983340c | ||
|
|
2aa3aaf2f8 | ||
|
|
6b2bf369ff | ||
|
|
fea5818951 | ||
|
|
e6eb71a484 | ||
|
|
1e023500be | ||
|
|
2c078de2c0 | ||
|
|
b373881b8b | ||
|
|
6b37cf0dda | ||
|
|
1025c4ec6e | ||
|
|
c44f9f685d | ||
|
|
5633d4504f | ||
|
|
c7aa49e541 | ||
|
|
ee139463d2 | ||
|
|
2cf7f0cc27 | ||
|
|
210549445e | ||
|
|
5b85158a7c | ||
|
|
09cba1a1df | ||
|
|
d75e2e85c9 | ||
|
|
966673dfd1 | ||
|
|
b8ad9265b9 | ||
|
|
b2f1fda641 | ||
|
|
1544b656e0 | ||
|
|
d97314c484 | ||
|
|
1432553897 | ||
|
|
f585fb0381 | ||
|
|
98557be52d | ||
|
|
e7da0a35d3 | ||
|
|
965a7f91ed | ||
|
|
2b1c973177 | ||
|
|
7e87cf7fdc | ||
|
|
c8cd5a4dc8 | ||
|
|
d00f7f6255 | ||
|
|
e8690e3419 | ||
|
|
72ff45c722 | ||
|
|
22a6271423 | ||
|
|
a6032ffd72 | ||
|
|
69ea9992ce | ||
|
|
36b4d69c2d | ||
|
|
2129533aa9 | ||
|
|
e635ace338 | ||
|
|
e729086015 | ||
|
|
3fbb7ce548 | ||
|
|
e0c31d6fdd | ||
|
|
d19c5b9775 | ||
|
|
f01b88083f | ||
|
|
d3ab1d1d2a | ||
|
|
7855442d33 | ||
|
|
bbaa6c76b4 | ||
|
|
cf71809f6c | ||
|
|
3b7d0ddb36 | ||
|
|
482fea91f3 | ||
|
|
4bace10bab | ||
|
|
603b7da9e6 | ||
|
|
bf4d168c96 | ||
|
|
756bb82fb7 | ||
|
|
81b1005b02 | ||
|
|
7ba52d1358 | ||
|
|
fcfb2c7e01 | ||
|
|
fa0a5afa63 | ||
|
|
b881b236a0 | ||
|
|
ec9c40bd64 | ||
|
|
9c59cbbc40 | ||
|
|
6dce41a070 | ||
|
|
dffeb05f86 | ||
|
|
ecfbfd83b9 | ||
|
|
50666c1307 | ||
|
|
200c9bd9eb | ||
|
|
2f93c6fdc8 | ||
|
|
5c21c0a498 | ||
|
|
67d47c283e | ||
|
|
dd59aab328 | ||
|
|
7a12fd701e | ||
|
|
9396a06a89 | ||
|
|
4cfb10db7c | ||
|
|
50024b62cb | ||
|
|
f31084baa7 | ||
|
|
2688becb05 | ||
|
|
8298900f76 | ||
|
|
b0c18a8ece | ||
|
|
255677d98e | ||
|
|
2a92ea4efd | ||
|
|
608b4f3009 | ||
|
|
a69cb1bf9c | ||
|
|
be40b3d160 | ||
|
|
51147b17cd | ||
|
|
5d0c2ae0b9 | ||
|
|
4d4af5c278 | ||
|
|
9982d7b7b4 | ||
|
|
22497199c5 | ||
|
|
873b180dab | ||
|
|
ca8e505cd0 | ||
|
|
465e1af838 | ||
|
|
41094c80d5 | ||
|
|
583a68cabe | ||
|
|
9c1a02dbd5 | ||
|
|
637a19ee21 | ||
|
|
896420064f | ||
|
|
b59a3fb5de | ||
|
|
0a7f8631b6 |
@@ -113,7 +113,7 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
BUILD_TOOLS_VERSION: "36.0.0"
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
BUILD_TOOLS_VERSION: "36.0.0"
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
|
||||
4
.github/workflows/android.publish.yml
vendored
4
.github/workflows/android.publish.yml
vendored
@@ -113,7 +113,7 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
BUILD_TOOLS_VERSION: "36.0.0"
|
||||
|
||||
- name: Build apks for Github release
|
||||
run: yarn release:android
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
keyStorePassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
keyPassword: ${{ secrets.PUBLIC_KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: "33.0.0"
|
||||
BUILD_TOOLS_VERSION: "36.0.0"
|
||||
|
||||
- name: Rename apk files
|
||||
run: |
|
||||
|
||||
577
apps/desktop/package-lock.json
generated
577
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.7",
|
||||
"version": "3.3.8",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/cjs/index.js",
|
||||
|
||||
@@ -39,7 +39,7 @@ const LINUX_AUTOSTART_DIRECTORY_PATH = path.join(
|
||||
"autostart"
|
||||
);
|
||||
|
||||
const STARTUP_ARGS = ["--hidden"];
|
||||
const HIDDEN_ARG = "--hidden";
|
||||
|
||||
export class AutoLaunch {
|
||||
static enable(hidden: boolean) {
|
||||
@@ -54,13 +54,13 @@ export class AutoLaunch {
|
||||
);
|
||||
} else {
|
||||
const loginItemSettings = app.getLoginItemSettings({
|
||||
args: STARTUP_ARGS
|
||||
args: hidden ? [HIDDEN_ARG] : undefined
|
||||
});
|
||||
if (loginItemSettings.openAtLogin) return;
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: true,
|
||||
openAsHidden: hidden,
|
||||
args: STARTUP_ARGS
|
||||
args: hidden ? [HIDDEN_ARG] : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3088
|
||||
versionCode 3091
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -6,7 +6,14 @@ import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
@@ -29,6 +36,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
@@ -237,6 +245,221 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void addShortcut(final String id, final String type, final String title, final String description, final String color, Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
String uri = "https://app.notesnook.com/open_" + type + "?id=" + id;
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri));
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
|
||||
Icon icon = createLetterIcon(type, title, color);
|
||||
ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, id)
|
||||
.setShortLabel(title)
|
||||
.setLongLabel(description != null && !description.isEmpty() ? description : title)
|
||||
.setIcon(icon)
|
||||
.setCategories(Set.of(type))
|
||||
.setIntent(intent)
|
||||
.build();
|
||||
|
||||
shortcutManager.requestPinShortcut(shortcut, null);
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeShortcut(final String id, Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
shortcutManager.disableShortcuts(java.util.Collections.singletonList(id));
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateShortcut(final String id, final String type, final String title, final String description,final String color, Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get existing shortcut to preserve icon and intent
|
||||
List<ShortcutInfo> shortcuts = shortcutManager.getPinnedShortcuts();
|
||||
ShortcutInfo existingShortcut = null;
|
||||
for (ShortcutInfo s : shortcuts) {
|
||||
if (s.getId().equals(id)) {
|
||||
existingShortcut = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingShortcut == null) {
|
||||
return;
|
||||
}
|
||||
Icon icon = createLetterIcon(type, title, color);
|
||||
ShortcutInfo updatedShortcut = new ShortcutInfo.Builder(mContext, id)
|
||||
.setShortLabel(title)
|
||||
.setIcon(icon)
|
||||
.setLongLabel(description != null && !description.isEmpty() ? description : title)
|
||||
.setIntent(existingShortcut.getIntent())
|
||||
.build();
|
||||
|
||||
shortcutManager.updateShortcuts(java.util.Collections.singletonList(updatedShortcut));
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeAllShortcuts(Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
promise.reject("UNSUPPORTED", "Pinned launcher shortcuts require Android 8.0 or higher");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
if (shortcutManager == null) {
|
||||
promise.reject("ERROR", "ShortcutManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
List<ShortcutInfo> pinnedShortcuts = shortcutManager.getPinnedShortcuts();
|
||||
if (!pinnedShortcuts.isEmpty()) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (ShortcutInfo shortcut : pinnedShortcuts) {
|
||||
ids.add(shortcut.getId());
|
||||
}
|
||||
shortcutManager.disableShortcuts(ids);
|
||||
}
|
||||
promise.resolve(true);
|
||||
} catch (Exception e) {
|
||||
promise.reject("ERROR", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void getAllShortcuts(Promise promise) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
ShortcutManager shortcutManager = mContext.getSystemService(ShortcutManager.class);
|
||||
WritableArray shortcuts = Arguments.createArray();
|
||||
List<ShortcutInfo> infos = shortcutManager.getPinnedShortcuts();
|
||||
|
||||
for (ShortcutInfo info: infos) {
|
||||
WritableMap data = Arguments.createMap();
|
||||
data.putString("id", info.getId());
|
||||
if (info.getShortLabel() != null) {
|
||||
data.putString("title", info.getShortLabel().toString());
|
||||
}
|
||||
|
||||
if (info.getLongLabel() != null) {
|
||||
data.putString("description", info.getLongLabel().toString());
|
||||
}
|
||||
|
||||
|
||||
if (!Objects.requireNonNull(info.getCategories()).isEmpty()) {
|
||||
if (info.getCategories().contains("note")) {
|
||||
data.putString("type", "note");
|
||||
} else if (info.getCategories().contains("notebook")) {
|
||||
data.putString("type", "notebook");
|
||||
} else if (info.getCategories().contains("tag")) {
|
||||
data.putString("type", "tag");
|
||||
} else if (info.getCategories().contains("color")) {
|
||||
data.putString("type", "color");
|
||||
}
|
||||
}
|
||||
shortcuts.pushMap(data);
|
||||
}
|
||||
promise.resolve(shortcuts);
|
||||
}
|
||||
|
||||
private Icon createLetterIcon(String type, String title, String colorCode) {
|
||||
|
||||
String letter = type.contains("tag") ? "#" : title != null && !title.isEmpty()
|
||||
? title.substring(0, 1).toUpperCase()
|
||||
: "?";
|
||||
int color = type.equals("color") ? Color.parseColor(colorCode) : getColorForLetter(letter);
|
||||
|
||||
if (type.equals("notebook")) return Icon.createWithResource(mContext, R.drawable.ic_notebook);
|
||||
// Use a larger canvas and fill it completely to avoid white borders from launcher masking.
|
||||
int iconSize = 256;
|
||||
Bitmap bitmap = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
if (type.equals("color")) {
|
||||
Paint backgroundPaint = new Paint();
|
||||
backgroundPaint.setColor(color);
|
||||
backgroundPaint.setAntiAlias(true);
|
||||
canvas.drawCircle(iconSize / 2, iconSize / 2, iconSize/2, backgroundPaint);
|
||||
} else {
|
||||
Paint textPaint = new Paint();
|
||||
textPaint.setColor(color);
|
||||
textPaint.setTextSize(130);
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setTextAlign(Paint.Align.CENTER);
|
||||
textPaint.setTypeface(android.graphics.Typeface.create(android.graphics.Typeface.DEFAULT, android.graphics.Typeface.BOLD));
|
||||
|
||||
float x = iconSize / 2f;
|
||||
float y = (iconSize / 2f) - ((textPaint.descent() + textPaint.ascent()) / 2f);
|
||||
|
||||
canvas.drawText(letter, x, y, textPaint);
|
||||
}
|
||||
|
||||
return Icon.createWithBitmap(bitmap);
|
||||
}
|
||||
|
||||
private int getColorForLetter(String letter) {
|
||||
int[] colors = {
|
||||
0xFF1976D2, // Blue
|
||||
0xFFD32F2F, // Red
|
||||
0xFF388E3C, // Green
|
||||
0xFFF57C00, // Orange
|
||||
0xFF7B1FA2, // Purple
|
||||
0xFF0097A7, // Cyan
|
||||
0xFFC2185B, // Pink
|
||||
0xFF455A64, // Blue Grey
|
||||
0xFF6A1B9A, // Deep Purple
|
||||
0xFF00796B, // Teal
|
||||
0xFF512DA8, // Indigo
|
||||
0xFF1565C0 // Dark Blue
|
||||
};
|
||||
|
||||
int hash = Math.abs(letter.hashCode());
|
||||
return colors[hash % colors.length];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="#008837"
|
||||
android:alpha="1">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,160Q720,160 720,160Q720,160 720,160L640,160L640,440L540,380L440,440L440,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800ZM440,440L540,380L640,440L640,440L540,380L440,440Z"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 284 B |
Binary file not shown.
|
After Width: | Height: | Size: 211 B |
Binary file not shown.
|
After Width: | Height: | Size: 440 B |
Binary file not shown.
|
After Width: | Height: | Size: 807 B |
@@ -1,3 +1,13 @@
|
||||
- Bug fixes and minor improvements
|
||||
- Add notebooks, tags and colors to Home Screen Shortcuts
|
||||
- Change day format and use /day in notes
|
||||
- Add Setting to change default editor line height
|
||||
- Set a custom title for monographs
|
||||
- Add webpage title and date clipped to web clips
|
||||
- Configure Week to start from Sunday or Monday
|
||||
- Change Note's creation date
|
||||
- Set expiry date on notes
|
||||
- Temporarily disable password change and recovery options
|
||||
- Note history now includes note title
|
||||
- Minor bug fixes
|
||||
|
||||
Thank you for using Notesnook!
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
useThemeEngineStore
|
||||
} from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { Appearance, I18nManager, StatusBar } from "react-native";
|
||||
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
@@ -43,7 +43,7 @@ import { changeSystemBarColors, useThemeStore } from "./stores/use-theme-store";
|
||||
import { useUserStore } from "./stores/use-user-store";
|
||||
import RNBootSplash from "react-native-bootsplash";
|
||||
import AppLocked from "./components/app-lock";
|
||||
|
||||
import { useSettingStore } from "./stores/use-setting-store";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
@@ -54,13 +54,18 @@ if (appLockEnabled || appLockMode !== "none") {
|
||||
RNBootSplash.hide({
|
||||
fade: true
|
||||
});
|
||||
Linking.getInitialURL().then((url) => {
|
||||
useSettingStore.setState({
|
||||
initialUrl: url
|
||||
});
|
||||
});
|
||||
const App = (props: { configureMode: "note-preview" }) => {
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
changeSystemBarColors();
|
||||
SettingsService.onFirstLaunch();
|
||||
changeSystemBarColors();
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
|
||||
@@ -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 { isImage, RequestOptions } from "@notesnook/core";
|
||||
import { isImage, RequestOptions, hosts } from "@notesnook/core";
|
||||
import { PermissionsAndroid, Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -32,6 +32,133 @@ import {
|
||||
getUploadedFileSize
|
||||
} from "./utils";
|
||||
import Upload from "@ammarahmed/react-native-upload";
|
||||
import { CloudUploader } from "react-native-nitro-cloud-uploader";
|
||||
|
||||
// Upload constants
|
||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB
|
||||
const MINIMUM_MULTIPART_FILE_SIZE = 25 * 1024 * 1024; // 25MB
|
||||
|
||||
interface InitiateMultipartResponse {
|
||||
uploadId: string;
|
||||
parts: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function initiateMultipartUpload(
|
||||
filename: string,
|
||||
fileSize: number,
|
||||
headers: Record<string, string>
|
||||
): Promise<InitiateMultipartResponse> {
|
||||
const totalParts = Math.ceil(fileSize / CHUNK_SIZE);
|
||||
|
||||
const url = `${hosts.API_HOST}/s3/multipart?name=${filename}&parts=${totalParts}&uploadId=`;
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to initiate multipart upload: ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (!data.uploadId || !data.parts) {
|
||||
throw new Error("Failed to initiate multipart upload: invalid response.");
|
||||
}
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Initiated multipart upload for ${filename} with upload ID: ${data.uploadId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function multipartUploadFile(
|
||||
filename: string,
|
||||
filePath: string,
|
||||
fileSize: number,
|
||||
requestOptions: RequestOptions,
|
||||
cancelToken: { cancel: (reason?: string) => Promise<void> }
|
||||
): Promise<Response> {
|
||||
const { headers } = requestOptions;
|
||||
|
||||
try {
|
||||
const uploadData = await initiateMultipartUpload(
|
||||
filename,
|
||||
fileSize,
|
||||
headers
|
||||
);
|
||||
const { uploadId, parts } = uploadData;
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Starting upload for ${filename} with ${parts.length} parts`
|
||||
);
|
||||
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
await CloudUploader.cancelUpload(uploadId);
|
||||
};
|
||||
|
||||
CloudUploader.addListener("upload-progress", (event) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(
|
||||
event.bytesUploaded || 0,
|
||||
event.totalBytes || fileSize,
|
||||
filename,
|
||||
0,
|
||||
"upload"
|
||||
);
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${event.bytesUploaded}/${
|
||||
event.totalBytes || fileSize
|
||||
}, chunk: ${event.chunkIndex}, progress: ${event.progress}`
|
||||
);
|
||||
});
|
||||
// CloudUploader handles chunking and uploading all parts internally
|
||||
const result = await CloudUploader.startUpload(
|
||||
filename,
|
||||
filePath,
|
||||
parts,
|
||||
3, // maxParallel
|
||||
true // showNotification
|
||||
);
|
||||
|
||||
CloudUploader.removeListener("upload-progress");
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error("Failed to upload multipart file");
|
||||
}
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Multipart upload completed for ${filename} with upload ID: ${uploadId}`
|
||||
);
|
||||
|
||||
const response = await fetch(`${hosts.API_HOST}/s3/multipart`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
Key: filename,
|
||||
UploadId: uploadId,
|
||||
PartETags: result.etags.map((etag, index) => ({
|
||||
partNumber: index + 1,
|
||||
etag: etag
|
||||
}))
|
||||
}),
|
||||
headers: { ...headers, "Content-Type": "application/json" }
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
DatabaseLogger.error(error, "Multipart upload failed", { filename });
|
||||
CloudUploader.removeListener("upload-progress");
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFile(
|
||||
filename: string,
|
||||
@@ -96,72 +223,111 @@ export async function uploadFile(
|
||||
}
|
||||
}
|
||||
|
||||
const upload = Upload.create({
|
||||
customUploadId: filename,
|
||||
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
|
||||
url: url,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...headers,
|
||||
"content-type": "application/octet-stream"
|
||||
},
|
||||
appGroup: IOS_APPGROUPID,
|
||||
notification: {
|
||||
filename:
|
||||
attachmentInfo && isImage(attachmentInfo?.mimeType)
|
||||
? "image"
|
||||
: attachmentInfo?.filename || "file",
|
||||
enabled: true,
|
||||
enableRingTone: true,
|
||||
autoClear: true
|
||||
}
|
||||
}).onChange((event) => {
|
||||
switch (event.status) {
|
||||
case "running":
|
||||
case "pending":
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(
|
||||
event.uploadedBytes || 0,
|
||||
event.totalBytes || fileInfo.size,
|
||||
filename,
|
||||
0,
|
||||
"upload"
|
||||
);
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${event.uploadedBytes}/${
|
||||
event.totalBytes || fileInfo.size
|
||||
}`
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
console.log("Upload completed");
|
||||
break;
|
||||
}
|
||||
});
|
||||
const result = await upload.start();
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
upload.cancel();
|
||||
};
|
||||
let uploaded = false;
|
||||
|
||||
const status = result.responseCode || 0;
|
||||
const uploaded = status >= 200 && status < 300;
|
||||
// Use multipart upload for files larger than MINIMUM_MULTIPART_FILE_SIZE
|
||||
if (fileInfo.size >= MINIMUM_MULTIPART_FILE_SIZE) {
|
||||
DatabaseLogger.info(
|
||||
`Using multipart upload for large file: ${filename} (${fileInfo.size} bytes)`
|
||||
);
|
||||
const result = await multipartUploadFile(
|
||||
filename,
|
||||
filePath,
|
||||
fileInfo.size,
|
||||
requestOptions,
|
||||
cancelToken
|
||||
);
|
||||
const status = result.status || 0;
|
||||
uploaded = status >= 200 && status < 300;
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(await result.text())}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Use single-part upload for smaller files
|
||||
DatabaseLogger.info(
|
||||
`Using single-part upload for file: ${filename} (${fileInfo.size} bytes)`
|
||||
);
|
||||
const upload = Upload.create({
|
||||
customUploadId: filename,
|
||||
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
|
||||
url: url,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
...headers,
|
||||
"content-type": "application/octet-stream"
|
||||
},
|
||||
appGroup: IOS_APPGROUPID,
|
||||
notification: {
|
||||
filename:
|
||||
attachmentInfo && isImage(attachmentInfo?.mimeType)
|
||||
? "image"
|
||||
: attachmentInfo?.filename || "file",
|
||||
enabled: true,
|
||||
enableRingTone: true,
|
||||
autoClear: true
|
||||
}
|
||||
}).onChange((event) => {
|
||||
switch (event.status) {
|
||||
case "running":
|
||||
case "pending":
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(
|
||||
event.uploadedBytes || 0,
|
||||
event.totalBytes || fileInfo.size,
|
||||
filename,
|
||||
0,
|
||||
"upload"
|
||||
);
|
||||
DatabaseLogger.info(
|
||||
`File upload progress: ${filename}, ${event.uploadedBytes}/${
|
||||
event.totalBytes || fileInfo.size
|
||||
}`
|
||||
);
|
||||
break;
|
||||
case "completed":
|
||||
DatabaseLogger.info("Upload completed");
|
||||
break;
|
||||
}
|
||||
});
|
||||
const result = await upload.start();
|
||||
cancelToken.cancel = async () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
upload.cancel();
|
||||
};
|
||||
|
||||
const status = result.responseCode || 0;
|
||||
uploaded = status >= 200 && status < 300;
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(result.error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(result.error)}`
|
||||
if (uploaded) {
|
||||
attachmentInfo = await db.attachments.attachment(filename);
|
||||
if (!attachmentInfo) return false;
|
||||
await checkUpload(
|
||||
filename,
|
||||
requestOptions.chunkSize,
|
||||
attachmentInfo.size
|
||||
);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, success`);
|
||||
}
|
||||
attachmentInfo = await db.attachments.attachment(filename);
|
||||
if (!attachmentInfo) return false;
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachmentInfo.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
@@ -25,7 +25,7 @@ import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -257,8 +257,13 @@ export const Login = ({
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading || !email.current) return;
|
||||
SheetManager.show("forgotpassword_sheet");
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message:
|
||||
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
|
||||
});
|
||||
// if (loading || !email.current) return;
|
||||
// SheetManager.show("forgotpassword_sheet");
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
|
||||
66
apps/mobile/app/components/date-picker/index.tsx
Normal file
66
apps/mobile/app/components/date-picker/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useRef } from "react";
|
||||
import { View } from "react-native";
|
||||
import { defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import DatePicker from "react-native-date-picker";
|
||||
import dayjs from "dayjs";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export default function DatePickerComponent(props: {
|
||||
onConfirm: (date: Date) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const dateRef = useRef<Date>(dayjs().add(1, "day").toDate());
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
borderRadius: defaultBorderRadius,
|
||||
padding: DefaultAppStyles.GAP,
|
||||
borderWidth: 0.5,
|
||||
borderColor: colors.primary.border,
|
||||
width: "80%",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<DatePicker
|
||||
theme={isDark ? "dark" : "light"}
|
||||
mode="date"
|
||||
minimumDate={dayjs().add(1, "day").toDate()}
|
||||
onCancel={() => {
|
||||
close?.();
|
||||
}}
|
||||
date={dateRef.current}
|
||||
onDateChange={(date) => {
|
||||
dateRef.current = date;
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.setExpiry()}
|
||||
type="accent"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!dateRef.current) return;
|
||||
props.onConfirm(dateRef.current);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="secondary"
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={async () => {
|
||||
props.onCancel();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -17,7 +17,6 @@ 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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { TextInput, View, ViewStyle } from "react-native";
|
||||
|
||||
@@ -167,14 +167,16 @@ const ColorPicker = ({
|
||||
);
|
||||
if (!title.current)
|
||||
return ToastManager.error(
|
||||
new Error(strings.allFieldsRequired())
|
||||
new Error(strings.allFieldsRequired()),
|
||||
"color-picker"
|
||||
);
|
||||
const exists = await db.colors.all.find((v) =>
|
||||
v.and([v(`colorCode`, "==", selectedColor)])
|
||||
);
|
||||
if (exists)
|
||||
return ToastManager.error(
|
||||
new Error(strings.colorExists(selectedColor))
|
||||
new Error(strings.colorExists(selectedColor)),
|
||||
"color-picker"
|
||||
);
|
||||
const id = await db.colors.add({
|
||||
title: title.current,
|
||||
|
||||
@@ -1,900 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { Component, createRef } from "react";
|
||||
import { InteractionManager, View } from "react-native";
|
||||
import Share from "react-native-share";
|
||||
import { notesnook } from "../../../../e2e/test.ids";
|
||||
import { db } from "../../../common/database";
|
||||
import BiometricService from "../../../services/biometrics";
|
||||
import { DDS } from "../../../services/device-detection";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import { getElevationStyle } from "../../../utils/elevation";
|
||||
import {
|
||||
eCloseActionSheet,
|
||||
eCloseVaultDialog,
|
||||
eOnLoadNote,
|
||||
eOpenVaultDialog,
|
||||
eUpdateNoteInEditor
|
||||
} from "../../../utils/events";
|
||||
import { deleteItems } from "../../../utils/functions";
|
||||
import { fluidTabsRef } from "../../../utils/global-refs";
|
||||
import { convertNoteToText } from "../../../utils/note-to-text";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogButtons from "../../dialog/dialog-buttons";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
export class VaultDialog extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
visible: false,
|
||||
wrongPassword: false,
|
||||
loading: false,
|
||||
note: {},
|
||||
vault: false,
|
||||
locked: true,
|
||||
permanant: false,
|
||||
goToEditor: false,
|
||||
share: false,
|
||||
passwordsDontMatch: false,
|
||||
deleteNote: false,
|
||||
focusIndex: null,
|
||||
biometricUnlock: false,
|
||||
isBiometryEnrolled: false,
|
||||
isBiometryAvailable: false,
|
||||
fingerprintAccess: false,
|
||||
changePassword: false,
|
||||
copyNote: false,
|
||||
revokeFingerprintAccess: false,
|
||||
title: strings.goToEditor(),
|
||||
description: null,
|
||||
clearVault: false,
|
||||
deleteVault: false,
|
||||
deleteAll: false,
|
||||
noteLocked: false
|
||||
};
|
||||
|
||||
this.passInputRef = createRef();
|
||||
this.confirmPassRef = createRef();
|
||||
this.changePassInputRef = createRef();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.newPassword = null;
|
||||
this.title = !this.state.novault
|
||||
? strings.createVault()
|
||||
: this.state.fingerprintAccess
|
||||
? strings.vaultFingerprintUnlock()
|
||||
: this.state.revokeFingerprintAccess
|
||||
? strings.revokeVaultFingerprintUnlock()
|
||||
: this.state.changePassword
|
||||
? strings.changeVaultPassword()
|
||||
: this.state.noteLocked
|
||||
? this.state.deleteNote
|
||||
? strings.deleteNote()
|
||||
: this.state.share
|
||||
? strings.shareNote()
|
||||
: this.state.copyNote
|
||||
? strings.copyNote()
|
||||
: this.state.goToEditor
|
||||
? strings.goToEditor()
|
||||
: strings.goToEditor()
|
||||
: strings.lockNote();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
eSubscribeEvent(eOpenVaultDialog, this.open);
|
||||
eSubscribeEvent(eCloseVaultDialog, this.close);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
eUnSubscribeEvent(eOpenVaultDialog, this.open);
|
||||
eUnSubscribeEvent(eCloseVaultDialog, this.close);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../../../services/event-manager').vaultType} data
|
||||
*/
|
||||
open = async (data) => {
|
||||
let biometry = await BiometricService.isBiometryAvailable();
|
||||
let available = false;
|
||||
let fingerprint = await BiometricService.hasInternetCredentials("nn_vault");
|
||||
|
||||
if (biometry) {
|
||||
available = true;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
note: data.item,
|
||||
novault: data.novault,
|
||||
locked: data.locked,
|
||||
permanant: data.permanant,
|
||||
goToEditor: data.goToEditor,
|
||||
share: data.share,
|
||||
deleteNote: data.deleteNote,
|
||||
copyNote: data.copyNote,
|
||||
isBiometryAvailable: available,
|
||||
biometricUnlock: fingerprint,
|
||||
isBiometryEnrolled: fingerprint,
|
||||
fingerprintAccess: data.fingerprintAccess,
|
||||
changePassword: data.changePassword,
|
||||
revokeFingerprintAccess: data.revokeFingerprintAccess,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
clearVault: data.clearVault,
|
||||
deleteVault: data.deleteVault,
|
||||
noteLocked: data.item && (await db.vaults.itemExists(data.item))
|
||||
});
|
||||
|
||||
if (
|
||||
fingerprint &&
|
||||
data.novault &&
|
||||
!data.fingerprintAccess &&
|
||||
!data.revokeFingerprintAccess &&
|
||||
!data.changePassword &&
|
||||
!data.clearVault &&
|
||||
!data.deleteVault
|
||||
) {
|
||||
await this._onPressFingerprintAuth(data.title, data.description);
|
||||
} else {
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
close = () => {
|
||||
if (this.state.loading) {
|
||||
ToastManager.show({
|
||||
heading: this.state.title,
|
||||
message: strings.pleaseWait() + "...",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.setState({
|
||||
visible: false,
|
||||
note: {},
|
||||
locked: false,
|
||||
permanant: false,
|
||||
goToEditor: false,
|
||||
share: false,
|
||||
novault: false,
|
||||
deleteNote: false,
|
||||
passwordsDontMatch: false
|
||||
});
|
||||
};
|
||||
|
||||
onPress = async () => {
|
||||
if (this.state.revokeFingerprintAccess) {
|
||||
await this._revokeFingerprintAccess();
|
||||
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (this.state.loading) return;
|
||||
|
||||
if (!this.password) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordNotEntered(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.novault) {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordNotMatched(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
passwordsDontMatch: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._createVault();
|
||||
} else if (this.state.changePassword) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
db.vault
|
||||
.changePassword(this.password, this.newPassword)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
if (this.state.biometricUnlock) {
|
||||
this._enrollFingerprint(this.newPassword);
|
||||
}
|
||||
ToastManager.show({
|
||||
heading: strings.passwordUpdated(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
if (e.message === db.vault.ERRORS.wrongPassword) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
} else {
|
||||
ToastManager.error(e);
|
||||
}
|
||||
});
|
||||
} else if (this.state.locked) {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
wrongPassword: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.state.noteLocked) {
|
||||
await this._unlockNote();
|
||||
} else {
|
||||
db.vault
|
||||
.unlock(this.password)
|
||||
.then(async () => {
|
||||
this.setState({
|
||||
wrongPassword: false
|
||||
});
|
||||
await this._lockNote();
|
||||
})
|
||||
.catch((e) => {
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
} else if (this.state.fingerprintAccess) {
|
||||
this._enrollFingerprint(this.password);
|
||||
} else if (this.state.clearVault) {
|
||||
await this.clearVault();
|
||||
} else if (this.state.deleteVault) {
|
||||
await this.deleteVault();
|
||||
}
|
||||
};
|
||||
|
||||
deleteVault = async () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
try {
|
||||
let verified = true;
|
||||
if (await db.user.getUser()) {
|
||||
verified = await db.user.verifyPassword(this.password);
|
||||
}
|
||||
if (verified) {
|
||||
let noteIds = [];
|
||||
if (this.state.deleteAll) {
|
||||
const vault = await db.vaults.default();
|
||||
const relations = await db.relations.from(vault, "note").get();
|
||||
noteIds = relations.map((item) => item.toId);
|
||||
}
|
||||
await db.vault.delete(this.state.deleteAll);
|
||||
|
||||
if (this.state.deleteAll) {
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.close();
|
||||
}, 100);
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
clearVault = async () => {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
try {
|
||||
const vault = await db.vaults.default();
|
||||
const relations = await db.relations.from(vault, "note").get();
|
||||
const noteIds = relations.map((item) => item.toId);
|
||||
|
||||
await db.vault.clear(this.password);
|
||||
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this.close();
|
||||
eSendEvent("vaultUpdated");
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
async _lockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await db.vault.add(this.state.note.id);
|
||||
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
message: strings.noteLocked(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _unlockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.state.permanant) {
|
||||
this._permanantUnlock();
|
||||
} else {
|
||||
await this._openNote();
|
||||
}
|
||||
}
|
||||
|
||||
_openNote = async () => {
|
||||
try {
|
||||
let note = await db.vault.open(this.state.note.id, this.password);
|
||||
if (this.state.biometricUnlock && !this.state.isBiometryEnrolled) {
|
||||
await this._enrollFingerprint(this.password);
|
||||
}
|
||||
|
||||
if (this.state.goToEditor) {
|
||||
this._openInEditor(note);
|
||||
} else if (this.state.share) {
|
||||
await this._shareNote(note);
|
||||
} else if (this.state.deleteNote) {
|
||||
await this._deleteNote();
|
||||
} else if (this.state.copyNote) {
|
||||
await this._copyNote(note);
|
||||
}
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
};
|
||||
async _deleteNote() {
|
||||
try {
|
||||
await db.vault.remove(this.state.note.id, this.password);
|
||||
await deleteItems("note", [this.state.note.id]);
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this._takeErrorAction(e);
|
||||
}
|
||||
}
|
||||
|
||||
async _enrollFingerprint(password) {
|
||||
this.setState(
|
||||
{
|
||||
loading: true
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
await db.vault.unlock(password);
|
||||
await BiometricService.storeCredentials(password);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: strings.biometricUnlockEnabled(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} catch (e) {
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async _createVault() {
|
||||
await db.vault.create(this.password);
|
||||
|
||||
if (this.state.biometricUnlock) {
|
||||
await this._enrollFingerprint(this.password);
|
||||
}
|
||||
if (this.state.note?.id) {
|
||||
await db.vault.add(this.state.note.id);
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: strings.noteLocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: strings.vaultCreated(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
}
|
||||
|
||||
_permanantUnlock() {
|
||||
db.vault
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.noteUnlocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
eSendEvent(eUpdateNoteInEditor, this.state.note, true);
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
|
||||
_openInEditor(note) {
|
||||
this.close();
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note
|
||||
});
|
||||
if (!DDS.isTab) {
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
ToastManager.show({
|
||||
heading: strings.noteCopied(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
async _shareNote(note) {
|
||||
this.close();
|
||||
try {
|
||||
await Share.open({
|
||||
heading: note.title,
|
||||
failOnCancel: false,
|
||||
message: (await convertNoteToText(note)) || ""
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
_takeErrorAction(e) {
|
||||
this.setState({
|
||||
wrongPassword: true,
|
||||
visible: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
_revokeFingerprintAccess = async () => {
|
||||
try {
|
||||
await BiometricService.resetCredentials();
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: strings.biometricUnlockDisabled(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: e.message,
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onPressFingerprintAuth = async (title, description) => {
|
||||
try {
|
||||
let credentials = await BiometricService.getCredentials(
|
||||
title || this.state.title,
|
||||
description || this.state.description
|
||||
);
|
||||
|
||||
if (credentials?.password) {
|
||||
this.password = credentials.password;
|
||||
this.onPress();
|
||||
} else {
|
||||
eSendEvent(eCloseActionSheet);
|
||||
await sleep(300);
|
||||
this.setState({
|
||||
visible: true
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { colors } = this.props;
|
||||
const {
|
||||
note,
|
||||
visible,
|
||||
novault,
|
||||
deleteNote,
|
||||
share,
|
||||
goToEditor,
|
||||
fingerprintAccess,
|
||||
changePassword,
|
||||
loading,
|
||||
deleteVault,
|
||||
clearVault
|
||||
} = this.state;
|
||||
|
||||
if (!visible) return null;
|
||||
return (
|
||||
<BaseDialog
|
||||
onShow={async () => {
|
||||
await sleep(100);
|
||||
|
||||
this.passInputRef.current?.focus();
|
||||
}}
|
||||
statusBarTranslucent={false}
|
||||
onRequestClose={this.close}
|
||||
visible={true}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: DDS.isTab ? 350 : "85%",
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader title={this.state.title} icon="shield" padding={12} />
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
{(novault ||
|
||||
changePassword ||
|
||||
this.state.clearVault ||
|
||||
this.state.deleteVault) &&
|
||||
!this.state.revokeFingerprintAccess ? (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={this.passInputRef}
|
||||
editable={!loading}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
onChangeText={(value) => {
|
||||
this.password = value;
|
||||
}}
|
||||
marginBottom={
|
||||
!this.state.biometricUnlock ||
|
||||
!this.state.isBiometryEnrolled ||
|
||||
!novault ||
|
||||
changePassword
|
||||
? 0
|
||||
: 10
|
||||
}
|
||||
onSubmit={() => {
|
||||
changePassword
|
||||
? this.confirmPassRef.current?.focus()
|
||||
: this.onPress;
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={
|
||||
changePassword ? strings.next() : this.state.title
|
||||
}
|
||||
returnKeyType={changePassword ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={
|
||||
changePassword
|
||||
? strings.currentPassword()
|
||||
: strings.password()
|
||||
}
|
||||
/>
|
||||
|
||||
{!this.state.biometricUnlock ||
|
||||
!this.state.isBiometryEnrolled ||
|
||||
!novault ||
|
||||
changePassword ? null : (
|
||||
<Button
|
||||
onPress={() =>
|
||||
this._onPressFingerprintAuth(strings.unlockNote(), "")
|
||||
}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
type="transparent"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{this.state.deleteVault && (
|
||||
<Button
|
||||
onPress={() =>
|
||||
this.setState({
|
||||
deleteAll: !this.state.deleteAll
|
||||
})
|
||||
}
|
||||
icon={
|
||||
this.state.deleteAll
|
||||
? "check-circle-outline"
|
||||
: "checkbox-blank-circle-outline"
|
||||
}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
width="100%"
|
||||
title={strings.deleteAllNotes()}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{changePassword ? (
|
||||
<>
|
||||
<Seperator half />
|
||||
<Input
|
||||
ref={this.confirmPassRef}
|
||||
editable={!loading}
|
||||
testID={notesnook.ids.dialogs.vault.changePwd}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(value) => {
|
||||
this.newPassword = value;
|
||||
}}
|
||||
autoComplete="password"
|
||||
onSubmit={this.onPress}
|
||||
returnKeyLabel="Change"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={strings.newPassword()}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{!novault ? (
|
||||
<View>
|
||||
<Input
|
||||
fwdRef={this.passInputRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwd}
|
||||
onChangeText={(value) => {
|
||||
this.password = value;
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
onSubmit={() => {
|
||||
this.confirmPassRef.current?.focus();
|
||||
}}
|
||||
placeholder={strings.password()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={this.confirmPassRef}
|
||||
autoCapitalize="none"
|
||||
testID={notesnook.ids.dialogs.vault.pwdAlt}
|
||||
secureTextEntry
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => this.password}
|
||||
errorMessage="Passwords do not match."
|
||||
onErrorCheck={() => null}
|
||||
marginBottom={0}
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Create"
|
||||
returnKeyType="done"
|
||||
onChangeText={(value) => {
|
||||
this.confirmPassword = value;
|
||||
if (value !== this.password) {
|
||||
this.setState({
|
||||
passwordsDontMatch: true
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
passwordsDontMatch: false
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSubmit={this.onPress}
|
||||
placeholder={strings.confirmPassword()}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{this.state.biometricUnlock &&
|
||||
!this.state.isBiometryEnrolled &&
|
||||
novault ? (
|
||||
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{this.state.isBiometryAvailable &&
|
||||
!this.state.fingerprintAccess &&
|
||||
!this.state.clearVault &&
|
||||
!this.state.deleteVault &&
|
||||
((!this.state.biometricUnlock && !changePassword) || !novault) ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
this.setState({
|
||||
biometricUnlock: !this.state.biometricUnlock
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
iconColor={
|
||||
this.state.biometricUnlock
|
||||
? colors.selected.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
type={this.state.biometricUnlock ? "transparent" : "plain"}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<DialogButtons
|
||||
onPressNegative={this.close}
|
||||
onPressPositive={this.onPress}
|
||||
loading={loading}
|
||||
positiveType={
|
||||
deleteVault || clearVault ? "errorShade" : "transparent"
|
||||
}
|
||||
positiveTitle={
|
||||
deleteVault
|
||||
? strings.delete()
|
||||
: clearVault
|
||||
? strings.clear()
|
||||
: fingerprintAccess
|
||||
? strings.enable()
|
||||
: this.state.revokeFingerprintAccess
|
||||
? strings.revoke()
|
||||
: changePassword
|
||||
? strings.change()
|
||||
: this.state.noteLocked
|
||||
? deleteNote
|
||||
? strings.delete()
|
||||
: share
|
||||
? strings.share()
|
||||
: goToEditor
|
||||
? strings.open()
|
||||
: strings.unlock()
|
||||
: !note.id
|
||||
? strings.create()
|
||||
: strings.lock()
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
1001
apps/mobile/app/components/dialogs/vault/index.tsx
Normal file
1001
apps/mobile/app/components/dialogs/vault/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@ export const Header = ({
|
||||
onLeftMenuButtonPress?: () => void;
|
||||
renderedInRoute?: RouteName;
|
||||
id?: string;
|
||||
title: string;
|
||||
title?: string;
|
||||
canGoBack?: boolean;
|
||||
onPressDefaultRightButton?: () => void;
|
||||
hasSearch?: boolean;
|
||||
@@ -114,7 +114,16 @@ export const Header = ({
|
||||
onLeftButtonPress={onLeftMenuButtonPress}
|
||||
/>
|
||||
|
||||
{hasSearch ? (
|
||||
{!title ? (
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
backgroundColor: colors.primary.hover,
|
||||
height: 10,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : hasSearch ? (
|
||||
<Paragraph>
|
||||
{selectionMode
|
||||
? `${selectedItemsList.length} selected`
|
||||
|
||||
@@ -131,7 +131,7 @@ function ReorderableList<T extends { id: string }>({
|
||||
]
|
||||
);
|
||||
|
||||
function getOrderedItems() {
|
||||
const getOrderedItems = React.useCallback(() => {
|
||||
if (!customizableSidebarFeature?.isAllowed) return data;
|
||||
const items: T[] = [];
|
||||
itemOrderState.forEach((id) => {
|
||||
@@ -142,7 +142,7 @@ function ReorderableList<T extends { id: string }>({
|
||||
|
||||
items.push(...data.filter((i) => !itemOrderState.includes(i.id)));
|
||||
return items;
|
||||
}
|
||||
}, [data, customizableSidebarFeature?.isAllowed]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
||||
@@ -18,8 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import {
|
||||
EncryptedContentItem,
|
||||
isEncryptedContent,
|
||||
Note,
|
||||
UnencryptedContentItem
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import KeepAwake from "@sayem314/react-native-keep-awake";
|
||||
import { diff } from "diffblazer";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { SafeAreaView, Text, View } from "react-native";
|
||||
import Animated from "react-native-reanimated";
|
||||
@@ -32,13 +40,16 @@ import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
eUnSubscribeEvent,
|
||||
openVault
|
||||
} from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eOnLoadNote, eShowMergeDialog } from "../../utils/events";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Dialog } from "../dialog";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import DialogButtons from "../dialog/dialog-buttons";
|
||||
import DialogContainer from "../dialog/dialog-container";
|
||||
@@ -47,46 +58,55 @@ import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { diff } from "diffblazer";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [keep, setKeep] = useState(null);
|
||||
const [copy, setCopy] = useState(null);
|
||||
const [selectedContent, setSelectedContent] =
|
||||
useState<UnencryptedContentItem>();
|
||||
const [copyOfDiscardedContent, setCopyOfDiscardedContent] =
|
||||
useState<UnencryptedContentItem>();
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const content = useRef(null);
|
||||
const isKeepingConflicted = !keep?.conflicted;
|
||||
const isKeeping = !!keep;
|
||||
const content = useRef<UnencryptedContentItem>(null);
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
|
||||
const applyChanges = async () => {
|
||||
let _content = keep;
|
||||
let note = await db.notes.note(_content.noteId);
|
||||
let contentToSave = selectedContent;
|
||||
if (!contentToSave) return;
|
||||
let note = await db.notes.note(contentToSave.noteId);
|
||||
if (!note) return;
|
||||
await db.notes.add({
|
||||
id: note.id,
|
||||
conflicted: false,
|
||||
dateEdited: _content.dateEdited
|
||||
dateEdited: contentToSave.dateEdited
|
||||
});
|
||||
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: _content.data,
|
||||
type: _content.type,
|
||||
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
|
||||
sessionId: Date.now(),
|
||||
conflicted: false
|
||||
});
|
||||
const noteLocked = await db.vaults.itemExists(note);
|
||||
|
||||
if (copy) {
|
||||
if (noteLocked) {
|
||||
await db.vault.save({
|
||||
...contentToSave,
|
||||
sessionId: `${Date.now()}`
|
||||
});
|
||||
} else {
|
||||
await db.content.add({
|
||||
id: note.contentId,
|
||||
data: contentToSave.data,
|
||||
type: contentToSave.type,
|
||||
dateResolved: content.current?.conflicted?.dateModified || Date.now(),
|
||||
sessionId: `${Date.now()}`,
|
||||
conflicted: undefined
|
||||
});
|
||||
}
|
||||
|
||||
if (copyOfDiscardedContent) {
|
||||
await db.notes.add({
|
||||
title: note.title + " (Copy)",
|
||||
content: {
|
||||
data: copy.data,
|
||||
type: copy.type
|
||||
data: copyOfDiscardedContent.data,
|
||||
type: copyOfDiscardedContent.type
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -103,15 +123,86 @@ const MergeConflicts = () => {
|
||||
Sync.run();
|
||||
};
|
||||
|
||||
const show = async (item) => {
|
||||
let noteContent = await db.content.get(item.contentId);
|
||||
content.current = { ...noteContent };
|
||||
if (__DEV__) {
|
||||
if (!noteContent.conflicted) {
|
||||
content.current.conflicted = { ...noteContent };
|
||||
const show = async (item: Note) => {
|
||||
const isLocked = await db.vaults.itemExists(item);
|
||||
let noteContent: UnencryptedContentItem;
|
||||
if (isLocked) {
|
||||
openVault({
|
||||
item: item,
|
||||
novault: true,
|
||||
customActionTitle: "Unlock note",
|
||||
customActionParagraph: "Unlock note to merge conflicts",
|
||||
onUnlock: async (item, password) => {
|
||||
if (!item || !password) return;
|
||||
const currentContent = await db.content.get(item.contentId!);
|
||||
|
||||
try {
|
||||
noteContent = {
|
||||
...(await db.content.get(item.contentId!)),
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
content.current = noteContent;
|
||||
if (__DEV__) {
|
||||
if (!noteContent?.conflicted) {
|
||||
content.current.conflicted = noteContent;
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
} catch (e) {
|
||||
presentDialog({
|
||||
input: true,
|
||||
inputPlaceholder: strings.enterPassword(),
|
||||
title: strings.unlockIncomingNote(),
|
||||
paragraph: strings.unlockIncomingNoteDesc(),
|
||||
positiveText: "Unlock",
|
||||
positivePress: async (password) => {
|
||||
try {
|
||||
noteContent = {
|
||||
...(await db.content.get(item.contentId!)),
|
||||
...item.content,
|
||||
conflicted: currentContent?.conflicted
|
||||
? await db.vault.decryptContent(
|
||||
currentContent?.conflicted as EncryptedContentItem,
|
||||
password
|
||||
)
|
||||
: undefined
|
||||
} as UnencryptedContentItem;
|
||||
|
||||
content.current = noteContent;
|
||||
if (__DEV__) {
|
||||
if (!noteContent?.conflicted) {
|
||||
content.current.conflicted = noteContent;
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
noteContent = (await db.content.get(
|
||||
item.contentId!
|
||||
)) as UnencryptedContentItem;
|
||||
content.current = noteContent;
|
||||
if (__DEV__) {
|
||||
if (!noteContent?.conflicted) {
|
||||
content.current.conflicted = noteContent;
|
||||
}
|
||||
}
|
||||
setVisible(true);
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -123,8 +214,8 @@ const MergeConflicts = () => {
|
||||
|
||||
const close = () => {
|
||||
setVisible(false);
|
||||
setCopy(null);
|
||||
setKeep(null);
|
||||
setCopyOfDiscardedContent(undefined);
|
||||
setSelectedContent(undefined);
|
||||
setDialogVisible(false);
|
||||
};
|
||||
|
||||
@@ -134,6 +225,12 @@ const MergeConflicts = () => {
|
||||
back,
|
||||
isCurrent,
|
||||
contentToKeep
|
||||
}: {
|
||||
isDiscarded: boolean;
|
||||
keeping: boolean;
|
||||
back: boolean;
|
||||
isCurrent: boolean;
|
||||
contentToKeep: UnencryptedContentItem;
|
||||
}) => {
|
||||
return (
|
||||
<View
|
||||
@@ -194,7 +291,7 @@ const MergeConflicts = () => {
|
||||
{isDiscarded ? (
|
||||
<Button
|
||||
onPress={() => {
|
||||
setCopy(contentToKeep);
|
||||
setCopyOfDiscardedContent(contentToKeep);
|
||||
setDialogVisible(true);
|
||||
}}
|
||||
title={strings.saveACopy()}
|
||||
@@ -222,7 +319,6 @@ const MergeConflicts = () => {
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
fontSize={AppFontSize.xs}
|
||||
color={colors.error.paragraph}
|
||||
onPress={() => {
|
||||
setDialogVisible(true);
|
||||
}}
|
||||
@@ -244,7 +340,9 @@ const MergeConflicts = () => {
|
||||
keeping && !isDiscarded ? strings.undo() : strings.keep()
|
||||
}
|
||||
onPress={() => {
|
||||
setKeep(keeping && !isDiscarded ? null : contentToKeep);
|
||||
setSelectedContent(
|
||||
keeping && !isDiscarded ? undefined : contentToKeep
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
@@ -258,7 +356,6 @@ const MergeConflicts = () => {
|
||||
<BaseDialog
|
||||
statusBarTranslucent
|
||||
transparent={false}
|
||||
animationType="slide"
|
||||
animated={false}
|
||||
bounce={false}
|
||||
onRequestClose={() => {
|
||||
@@ -266,15 +363,9 @@ const MergeConflicts = () => {
|
||||
}}
|
||||
centered={false}
|
||||
background={colors?.primary.background}
|
||||
supportedOrientations={[
|
||||
"portrait",
|
||||
"portrait-upside-down",
|
||||
"landscape",
|
||||
"landscape-left",
|
||||
"landscape-right"
|
||||
]}
|
||||
visible={true}
|
||||
>
|
||||
<Dialog context="merge-conflicts" />
|
||||
<SafeAreaView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
@@ -301,15 +392,15 @@ const MergeConflicts = () => {
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : null
|
||||
backgroundColor: DDS.isLargeTablet() ? "rgba(0,0,0,0.3)" : undefined
|
||||
}}
|
||||
>
|
||||
<ConfigBar
|
||||
back={true}
|
||||
isCurrent={true}
|
||||
isDiscarded={isKeeping && isKeepingConflicted}
|
||||
keeping={isKeeping}
|
||||
contentToKeep={content.current}
|
||||
isDiscarded={!!selectedContent && !selectedContent.conflicted}
|
||||
keeping={!!selectedContent}
|
||||
contentToKeep={content.current!}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
@@ -323,15 +414,18 @@ const MergeConflicts = () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictPrimary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
const note = await db.notes.note(content.current!.noteId);
|
||||
if (!note) return;
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: diff(
|
||||
content.current.conflicted.data,
|
||||
content.current.data
|
||||
)
|
||||
});
|
||||
if (content.current && content.current.conflicted) {
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: diff(
|
||||
(content.current.conflicted as UnencryptedContentItem)
|
||||
.data,
|
||||
content.current.data
|
||||
)
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
@@ -339,9 +433,11 @@ const MergeConflicts = () => {
|
||||
<ConfigBar
|
||||
back={false}
|
||||
isCurrent={false}
|
||||
isDiscarded={isKeeping && !isKeepingConflicted}
|
||||
keeping={isKeeping}
|
||||
contentToKeep={content.current.conflicted}
|
||||
isDiscarded={!!selectedContent && !!selectedContent.conflicted}
|
||||
keeping={!!selectedContent}
|
||||
contentToKeep={
|
||||
content.current!.conflicted! as UnencryptedContentItem
|
||||
}
|
||||
/>
|
||||
|
||||
<Animated.View
|
||||
@@ -354,11 +450,12 @@ const MergeConflicts = () => {
|
||||
<ReadonlyEditor
|
||||
editorId="conflictSecondary"
|
||||
onLoad={async (loadContent) => {
|
||||
const note = await db.notes.note(content.current?.noteId);
|
||||
const note = await db.notes.note(content.current?.noteId!);
|
||||
if (!note) return;
|
||||
loadContent({
|
||||
id: note.id,
|
||||
data: content.current.conflicted.data
|
||||
data: (content.current!.conflicted as UnencryptedContentItem)
|
||||
.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@@ -47,7 +47,7 @@ const HistoryItem = ({
|
||||
}: {
|
||||
index: number;
|
||||
items?: VirtualizedGrouping<HistorySession>;
|
||||
note?: Note;
|
||||
note: Note;
|
||||
}) => {
|
||||
const [item] = useDBItem(index, "noteHistory", items);
|
||||
const { colors } = useThemeColors();
|
||||
@@ -73,6 +73,7 @@ const HistoryItem = ({
|
||||
session: getDate(item.dateCreated, item.dateModified)
|
||||
}}
|
||||
content={content}
|
||||
note={note}
|
||||
/>
|
||||
),
|
||||
context: "note_history"
|
||||
@@ -135,7 +136,7 @@ export default function NoteHistory({
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ index }: { index: number }) => (
|
||||
<HistoryItem index={index} items={history} />
|
||||
<HistoryItem index={index} items={history} note={note} />
|
||||
),
|
||||
[history]
|
||||
);
|
||||
|
||||
@@ -37,21 +37,43 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { diff } from "diffblazer";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import {
|
||||
HistorySession,
|
||||
isEncryptedContent,
|
||||
Note,
|
||||
NoteContent,
|
||||
SessionContentItem,
|
||||
TrashOrItem
|
||||
} from "@notesnook/core";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function NotePreview({ session, content, note }) {
|
||||
export default function NotePreview({
|
||||
session,
|
||||
content,
|
||||
note
|
||||
}: {
|
||||
session?: HistorySession & { session: string };
|
||||
content:
|
||||
| Partial<
|
||||
NoteContent<boolean> & {
|
||||
title: string;
|
||||
}
|
||||
>
|
||||
| undefined;
|
||||
note: TrashOrItem<Note>;
|
||||
}) {
|
||||
const { colors } = useThemeColors();
|
||||
const [locked, setLocked] = useState(false);
|
||||
|
||||
async function restore() {
|
||||
if (note && note.type === "trash") {
|
||||
if ((await db.trash.restore(note.id)) === false) return;
|
||||
await db.trash.restore(note.id);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
useSelectionStore.getState().setSelectionMode();
|
||||
ToastManager.show({
|
||||
heading: strings.noteRestored(),
|
||||
type: "success"
|
||||
@@ -59,24 +81,26 @@ export default function NotePreview({ session, content, note }) {
|
||||
eSendEvent(eCloseSheet);
|
||||
return;
|
||||
}
|
||||
await db.noteHistory.restore(session.id);
|
||||
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
|
||||
const note = editorController.current.note.current[session?.noteId];
|
||||
if (note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note,
|
||||
forced: true
|
||||
});
|
||||
if (session) {
|
||||
await db.noteHistory.restore(session.id);
|
||||
if (useTabStore.getState().hasTabForNote(session?.noteId)) {
|
||||
const note = editorController.current.note.current[session?.noteId];
|
||||
if (note) {
|
||||
eSendEvent(eOnLoadNote, {
|
||||
item: note,
|
||||
forced: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
eSendEvent(eCloseSheet, "note_history");
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
eSendEvent(eCloseSheet, "note_history");
|
||||
eSendEvent(eCloseSheet);
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
ToastManager.show({
|
||||
heading: strings.noteRestoredFromHistory(),
|
||||
type: "success"
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: strings.noteRestoredFromHistory(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,15 +115,17 @@ export default function NotePreview({ session, content, note }) {
|
||||
negativeText: strings.cancel(),
|
||||
context: "local",
|
||||
positivePress: async () => {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().refresh();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastManager.show({
|
||||
heading: strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
eSendEvent(eCloseSheet);
|
||||
if (note) {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().refresh();
|
||||
useSelectionStore.getState().setSelectionMode();
|
||||
ToastManager.show({
|
||||
heading: strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
eSendEvent(eCloseSheet);
|
||||
}
|
||||
},
|
||||
positiveType: "error"
|
||||
});
|
||||
@@ -113,8 +139,11 @@ export default function NotePreview({ session, content, note }) {
|
||||
}}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<DialogHeader padding={12} title={note?.title || session?.session} />
|
||||
{!session?.locked && !locked ? (
|
||||
<DialogHeader
|
||||
padding={12}
|
||||
title={content?.title || note.title || session?.session}
|
||||
/>
|
||||
{!session?.locked && !locked && content?.data ? (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -125,17 +154,27 @@ export default function NotePreview({ session, content, note }) {
|
||||
editorId="historyPreview"
|
||||
onLoad={async (loadContent) => {
|
||||
try {
|
||||
if (content.data) {
|
||||
const _note = note || (await db.notes.note(session?.noteId));
|
||||
const currentContent = await db.content.get(_note.contentId);
|
||||
loadContent({
|
||||
data: diff(currentContent.data, content.data),
|
||||
id: _note.id
|
||||
});
|
||||
if (content?.data) {
|
||||
const currentContent = note?.contentId
|
||||
? await db.content.get(note.contentId)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
currentContent?.data &&
|
||||
!isEncryptedContent(currentContent)
|
||||
) {
|
||||
loadContent({
|
||||
data: diff(
|
||||
currentContent?.data || "<p></p>",
|
||||
content.data as string
|
||||
),
|
||||
id: session?.noteId || note.id
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(
|
||||
e,
|
||||
e as Error,
|
||||
"Failed to load history preview",
|
||||
"local"
|
||||
);
|
||||
@@ -153,7 +192,9 @@ export default function NotePreview({ session, content, note }) {
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.encryptedNoteHistoryNotice()}
|
||||
{!content?.data
|
||||
? strings.noContent()
|
||||
: strings.encryptedNoteHistoryNotice()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
)}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
export const DateMeta = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
function getDateMeta() {
|
||||
let keys = Object.keys(item);
|
||||
if (keys.includes("dateEdited"))
|
||||
keys.splice(
|
||||
keys.findIndex((k) => k === "dateModified"),
|
||||
1
|
||||
);
|
||||
return keys.filter((key) => key.startsWith("date") && key !== "date");
|
||||
}
|
||||
|
||||
const renderItem = (key) =>
|
||||
!item[key] ? null : (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.dateDescFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.primary.border,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
{getDateMeta().map(renderItem)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
130
apps/mobile/app/components/properties/date-meta.tsx
Normal file
130
apps/mobile/app/components/properties/date-meta.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
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 React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import DateTimePickerModal from "react-native-modal-datetime-picker";
|
||||
import { db } from "../../common/database";
|
||||
import { Item, Note } from "@notesnook/core";
|
||||
import AppIcon from "../ui/AppIcon";
|
||||
export const DateMeta = ({ item }: { item: Item }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
|
||||
const [dateCreated, setDateCreated] = useState(item.dateCreated);
|
||||
|
||||
function getDateMeta() {
|
||||
let keys = Object.keys(item);
|
||||
if (keys.includes("dateEdited"))
|
||||
keys.splice(
|
||||
keys.findIndex((k) => k === "dateModified"),
|
||||
1
|
||||
);
|
||||
return keys.filter((key) => key.startsWith("date") && key !== "date");
|
||||
}
|
||||
|
||||
const renderItem = (key: string) =>
|
||||
!item[key as keyof Item] ? null : (
|
||||
<View
|
||||
key={key}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2
|
||||
}}
|
||||
>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.dateDescFromKey(
|
||||
key as
|
||||
| "dateDeleted"
|
||||
| "dateEdited"
|
||||
| "dateModified"
|
||||
| "dateCreated"
|
||||
| "dateUploaded"
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
onPress={
|
||||
item.type !== "note"
|
||||
? undefined
|
||||
: () => {
|
||||
setIsDatePickerVisible(true);
|
||||
}
|
||||
}
|
||||
>
|
||||
{getFormattedDate(
|
||||
key === "dateCreated"
|
||||
? dateCreated
|
||||
: (item[key as keyof Item] as string),
|
||||
"date-time"
|
||||
)}
|
||||
{key === "dateCreated" && item.type === "note" ? (
|
||||
<>
|
||||
{" "}
|
||||
<AppIcon name="pencil" size={AppFontSize.md} />
|
||||
</>
|
||||
) : null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{item.type === "note" ? (
|
||||
<DateTimePickerModal
|
||||
isVisible={isDatePickerVisible}
|
||||
mode="datetime"
|
||||
onConfirm={async (date: Date) => {
|
||||
await db.notes.add({
|
||||
id: item.id,
|
||||
dateCreated: date.getTime()
|
||||
});
|
||||
setDateCreated(date.getTime());
|
||||
setIsDatePickerVisible(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDatePickerVisible(false);
|
||||
}}
|
||||
maximumDate={new Date((item as Note).dateEdited)}
|
||||
isDarkModeEnabled={isDark}
|
||||
is24Hour={db.settings.getTimeFormat() === "24-hour"}
|
||||
date={new Date(dateCreated)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.primary.border,
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL_SMALL
|
||||
}}
|
||||
>
|
||||
{getDateMeta().map(renderItem)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -39,6 +39,7 @@ import { DateMeta } from "./date-meta";
|
||||
import { Items } from "./items";
|
||||
import Notebooks from "./notebooks";
|
||||
import { TagStrip, Tags } from "./tags";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -203,6 +204,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
/>
|
||||
) : null}
|
||||
<SheetProvider context="properties" />
|
||||
<Dialog context="properties" />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,7 @@ import AppIcon from "../ui/AppIcon";
|
||||
import { Button } from "../ui/button";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Dialog } from "../dialog";
|
||||
|
||||
const TOP_BAR_ITEMS: ActionId[] = [
|
||||
"pin",
|
||||
@@ -57,6 +58,8 @@ const BOTTOM_BAR_ITEMS: ActionId[] = [
|
||||
"export",
|
||||
"copy-link",
|
||||
"duplicate",
|
||||
"launcher-shortcut",
|
||||
"expiry-date",
|
||||
"trash"
|
||||
];
|
||||
|
||||
@@ -76,6 +79,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
|
||||
"reorder",
|
||||
"rename-color",
|
||||
"rename-tag",
|
||||
"launcher-shortcut",
|
||||
"restore",
|
||||
"trash",
|
||||
"delete"
|
||||
@@ -170,15 +174,15 @@ export const Items = ({
|
||||
DDS.isTab
|
||||
? AppFontSize.xxl
|
||||
: shouldShrink
|
||||
? AppFontSize.xxl
|
||||
: AppFontSize.lg
|
||||
? AppFontSize.xxl
|
||||
: AppFontSize.lg
|
||||
}
|
||||
color={
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id.match(/(delete|trash)/g)
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
</Pressable>
|
||||
@@ -209,8 +213,8 @@ export const Items = ({
|
||||
text: item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
? colors.error.paragraph
|
||||
: colors.primary.paragraph
|
||||
}}
|
||||
testID={"icon-" + item.id}
|
||||
onPress={item.onPress}
|
||||
@@ -274,8 +278,8 @@ export const Items = ({
|
||||
item.checked
|
||||
? item.activeColor || colors.primary.accent
|
||||
: item.id === "delete" || item.id === "trash"
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
? colors.error.icon
|
||||
: colors.secondary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -78,12 +78,15 @@ const PublishNoteSheet = ({
|
||||
const isFeatureAvailable = useIsFeatureAvailable("monographAnalytics");
|
||||
const [isLocked, setIsLocked] = useState(false);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const customTitle = useRef<string>("");
|
||||
const pwdInput = useRef<TextInput>(null);
|
||||
const titleInput = useRef<TextInput>(null);
|
||||
const passwordValue = useRef<string>(undefined);
|
||||
const monographData = useAsync(async () => {
|
||||
return fetchMonographData(note?.id);
|
||||
}, []);
|
||||
const monograph = monographData.result?.monograph;
|
||||
customTitle.current = monograph?.title || note.title || "";
|
||||
const publishUrl = monograph && `${hosts.MONOGRAPH_HOST}/${monograph?.id}`;
|
||||
const isPublished = db.monographs.monograph(note?.id);
|
||||
|
||||
@@ -108,7 +111,7 @@ const PublishNoteSheet = ({
|
||||
try {
|
||||
if (note?.id) {
|
||||
if (isLocked && !passwordValue.current) return;
|
||||
await db.monographs.publish(note.id, {
|
||||
await db.monographs.publish(note.id, customTitle.current, {
|
||||
selfDestruct: selfDestruct,
|
||||
password: isLocked ? passwordValue.current : undefined
|
||||
});
|
||||
@@ -129,7 +132,6 @@ const PublishNoteSheet = ({
|
||||
|
||||
setPublishLoading(false);
|
||||
};
|
||||
|
||||
const setPublishLoading = (value: boolean) => {
|
||||
setPublishing(value);
|
||||
};
|
||||
@@ -245,6 +247,13 @@ const PublishNoteSheet = ({
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
fwdRef={titleInput}
|
||||
onChangeText={(value) => (customTitle.current = value)}
|
||||
defaultValue={customTitle.current}
|
||||
placeholder={strings.noteTitle()}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (publishing) return;
|
||||
|
||||
@@ -30,6 +30,7 @@ import ReorderableList from "../list/reorderable-list";
|
||||
import { Properties } from "../properties";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
import { MenuItem } from "./menu-item";
|
||||
import { Default_Drag_Action } from "../../hooks/use-actions";
|
||||
|
||||
export const ColorSection = React.memo(
|
||||
function ColorSection() {
|
||||
@@ -59,7 +60,7 @@ export const ColorSection = React.memo(
|
||||
|
||||
const onLongPress = React.useCallback((item: SideMenuItem) => {
|
||||
if (useSideBarDraggingStore.getState().dragging) return;
|
||||
Properties.present(item.data as Color);
|
||||
Properties.present(item.data as Color, false, [Default_Drag_Action]);
|
||||
}, []);
|
||||
|
||||
const renderIcon = React.useCallback((item: SideMenuItem, size: number) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ import { Pressable } from "../ui/pressable";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
|
||||
function _MenuItem({
|
||||
export function MenuItem({
|
||||
item,
|
||||
index,
|
||||
testID,
|
||||
@@ -179,12 +179,3 @@ function _MenuItem({
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
export const MenuItem = React.memo(_MenuItem, (prev, next) => {
|
||||
if (
|
||||
prev.item.id !== next.item.id &&
|
||||
prev.item.data?.dateModified !== next.item.data?.dateModified
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -31,6 +31,9 @@ import ReorderableList from "../list/reorderable-list";
|
||||
import { MenuItem } from "./menu-item";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { useSideBarDraggingStore } from "./dragging-store";
|
||||
import { Properties } from "../properties";
|
||||
import { Default_Drag_Action } from "../../hooks/use-actions";
|
||||
|
||||
export const PinnedSection = React.memo(
|
||||
function PinnedSection() {
|
||||
@@ -57,6 +60,11 @@ export const PinnedSection = React.memo(
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onLongPress = React.useCallback((item: SideMenuItem) => {
|
||||
if (useSideBarDraggingStore.getState().dragging) return;
|
||||
Properties.present(item.data as Notebook, false, [Default_Drag_Action]);
|
||||
}, []);
|
||||
|
||||
const menuItems = useMemo(
|
||||
() =>
|
||||
menuPins.map((item) => ({
|
||||
@@ -65,7 +73,8 @@ export const PinnedSection = React.memo(
|
||||
icon: item.type === "notebook" ? "notebook-outline" : "pound",
|
||||
dataType: item.type,
|
||||
data: item,
|
||||
onPress: onPress
|
||||
onPress: onPress,
|
||||
onLongPress: onLongPress
|
||||
})) as SideMenuItem[],
|
||||
[menuPins, onPress]
|
||||
);
|
||||
@@ -98,6 +107,7 @@ export const PinnedSection = React.memo(
|
||||
flexGrow: 1,
|
||||
width: "100%"
|
||||
}}
|
||||
disableDefaultDrag
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1
|
||||
}}
|
||||
|
||||
@@ -17,12 +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/>.
|
||||
*/
|
||||
/* eslint-disable no-inner-declarations */
|
||||
import { useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
|
||||
import {
|
||||
Color,
|
||||
createInternalLink,
|
||||
Item,
|
||||
ItemReference,
|
||||
Note,
|
||||
Notebook,
|
||||
VAULT_ERRORS
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -30,7 +32,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { DisplayedNotification } from "@notifee/react-native";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { InteractionManager, Platform } from "react-native";
|
||||
import { InteractionManager, Platform, View } from "react-native";
|
||||
import Share from "react-native-share";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { AttachmentDialog } from "../components/attachments";
|
||||
@@ -65,11 +67,13 @@ import { useSelectionStore } from "../stores/use-selection-store";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { useTagStore } from "../stores/use-tag-store";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import { eUpdateNoteInEditor } from "../utils/events";
|
||||
import { eCloseSheet, eUpdateNoteInEditor } from "../utils/events";
|
||||
import { deleteItems } from "../utils/functions";
|
||||
import { convertNoteToText } from "../utils/note-to-text";
|
||||
import { sleep } from "../utils/time";
|
||||
import { resetStoredState } from "./use-stored-state";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
|
||||
import DatePickerComponent from "../components/date-picker";
|
||||
|
||||
export type ActionId =
|
||||
| "select"
|
||||
@@ -114,7 +118,9 @@ export type ActionId =
|
||||
| "remove-from-notebook"
|
||||
| "trash"
|
||||
| "default-homepage"
|
||||
| "default-tag";
|
||||
| "default-tag"
|
||||
| "launcher-shortcut"
|
||||
| "expiry-date";
|
||||
|
||||
export type Action = {
|
||||
id: ActionId;
|
||||
@@ -130,6 +136,31 @@ export type Action = {
|
||||
locked?: boolean;
|
||||
};
|
||||
|
||||
export const Default_Drag_Action: Action = {
|
||||
id: "reorder",
|
||||
title: strings.reorder(),
|
||||
icon: "sort-ascending",
|
||||
onPress: async () => {
|
||||
const feature = await isFeatureAvailable("customizableSidebar");
|
||||
if (feature && !feature.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: feature.error,
|
||||
type: "info",
|
||||
context: "local",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => {
|
||||
PaywallSheet.present(feature);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
useSideBarDraggingStore.setState({
|
||||
dragging: true
|
||||
});
|
||||
eSendEvent(eCloseSheet);
|
||||
}
|
||||
};
|
||||
|
||||
function isNotePinnedInNotifications(item: Item) {
|
||||
const pinned = Notifications.getPinnedNotes();
|
||||
if (!pinned || pinned.length === 0) {
|
||||
@@ -158,10 +189,12 @@ export const useActions = ({
|
||||
"shortcuts",
|
||||
"notebooks",
|
||||
"customizableSidebar",
|
||||
"customHomepage"
|
||||
"customHomepage",
|
||||
"androidLauncherShortcuts",
|
||||
"expiringNotes"
|
||||
]);
|
||||
const [item, setItem] = useState(propItem);
|
||||
const { colors } = useThemeColors();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const setMenuPins = useMenuStore((state) => state.setMenuPins);
|
||||
const [isPinnedToMenu, setIsPinnedToMenu] = useState(
|
||||
db.shortcuts.exists(item.id)
|
||||
@@ -297,7 +330,6 @@ export const useActions = ({
|
||||
id: item.id,
|
||||
title: value
|
||||
});
|
||||
|
||||
eSendEvent(Navigation.routeNames.TaggedNotes);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
useTagStore.getState().refresh();
|
||||
@@ -435,31 +467,6 @@ export const useActions = ({
|
||||
icon: "square-edit-outline",
|
||||
onPress: renameColor
|
||||
});
|
||||
|
||||
actions.push({
|
||||
id: "reorder",
|
||||
title: strings.reorder(),
|
||||
icon: "sort-ascending",
|
||||
onPress: async () => {
|
||||
if (features && !features.customizableSidebar.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: features.customizableSidebar.error,
|
||||
type: "info",
|
||||
context: "local",
|
||||
actionText: strings.upgrade(),
|
||||
func: () => {
|
||||
PaywallSheet.present(features.customizableSidebar);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
useSideBarDraggingStore.setState({
|
||||
dragging: true
|
||||
});
|
||||
close();
|
||||
},
|
||||
locked: !features?.customizableSidebar.isAllowed
|
||||
});
|
||||
}
|
||||
|
||||
if (item.type === "reminder") {
|
||||
@@ -706,26 +713,6 @@ export const useActions = ({
|
||||
type: item.type
|
||||
}
|
||||
);
|
||||
|
||||
resetStoredState(
|
||||
"app-home-navigtion-key",
|
||||
isHomepage
|
||||
? undefined
|
||||
: {
|
||||
name:
|
||||
item.type === "notebook"
|
||||
? "Notebook"
|
||||
: item.type === "tag"
|
||||
? "TaggedNotes"
|
||||
: item.type === "color"
|
||||
? "ColorNotes"
|
||||
: undefined,
|
||||
params: {
|
||||
item: item,
|
||||
id: item.id
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -965,7 +952,7 @@ export const useActions = ({
|
||||
copyNote: true,
|
||||
novault: true,
|
||||
locked: true,
|
||||
item: item,
|
||||
item: item as Note,
|
||||
title: strings.copyNote()
|
||||
});
|
||||
} else {
|
||||
@@ -1177,6 +1164,45 @@ export const useActions = ({
|
||||
},
|
||||
checked: item.archived,
|
||||
isToggle: true
|
||||
},
|
||||
{
|
||||
id: "expiry-date",
|
||||
title: item.expiryDate ? strings.unsetExpiry() : strings.setExpiry(),
|
||||
icon: item.expiryDate ? "bomb-off" : "bomb",
|
||||
locked: !features?.expiringNotes?.isAllowed,
|
||||
onPress: async () => {
|
||||
if (item.expiryDate) {
|
||||
await db.notes.setExpiryDate(null, item.id);
|
||||
setItem((await db.notes.note(item.id)) as Item);
|
||||
} else {
|
||||
if (features && !features?.expiringNotes.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: features?.expiringNotes.error,
|
||||
type: "info",
|
||||
actionText: strings.upgrade(),
|
||||
context: "local",
|
||||
func: () => {
|
||||
PaywallSheet.present(features?.expiringNotes);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
presentDialog({
|
||||
context: "properties",
|
||||
component: (close) => (
|
||||
<DatePickerComponent
|
||||
onCancel={() => close?.()}
|
||||
onConfirm={async (date) => {
|
||||
close?.();
|
||||
await db.notes.setExpiryDate(date.getTime(), item.id);
|
||||
setItem((await db.notes.note(item.id)) as Item);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1207,5 +1233,44 @@ export const useActions = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
Platform.OS === "android" &&
|
||||
(item.type === "tag" ||
|
||||
item.type === "note" ||
|
||||
item.type === "notebook" ||
|
||||
item.type === "color")
|
||||
) {
|
||||
actions.push({
|
||||
id: "launcher-shortcut",
|
||||
title: strings.addToHome(),
|
||||
icon: "cellphone-arrow-down",
|
||||
locked: !features?.androidLauncherShortcuts.isAllowed,
|
||||
onPress: async () => {
|
||||
if (features && !features?.androidLauncherShortcuts.isAllowed) {
|
||||
ToastManager.show({
|
||||
message: features?.androidLauncherShortcuts.error,
|
||||
type: "info",
|
||||
actionText: strings.upgrade(),
|
||||
context: "local",
|
||||
func: () => {
|
||||
PaywallSheet.present(features?.androidLauncherShortcuts);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await NotesnookModule.addShortcut(
|
||||
item.id,
|
||||
item.type,
|
||||
item.title,
|
||||
(item as Note).headline || (item as Notebook).description || "",
|
||||
(item as Color).colorCode
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ import Notifications from "../services/notifications";
|
||||
import PremiumService from "../services/premium";
|
||||
import SettingsService from "../services/settings";
|
||||
import Sync from "../services/sync";
|
||||
import { clearAllStores, initAfterSync, initialize } from "../stores";
|
||||
import { clearAllStores, initAfterSync } from "../stores";
|
||||
import { refreshAllStores } from "../stores/create-db-collection-store";
|
||||
import { useAttachmentStore } from "../stores/use-attachment-store";
|
||||
import { useMessageStore } from "../stores/use-message-store";
|
||||
@@ -108,6 +108,8 @@ import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { sleep } from "../utils/time";
|
||||
import useFeatureManager from "./use-feature-manager";
|
||||
import { deleteDCacheFiles } from "../common/filesystem/io";
|
||||
import dayjs from "dayjs";
|
||||
import { useRelationStore } from "../stores/use-relation-store";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -160,7 +162,10 @@ const onUserSessionExpired = async () => {
|
||||
eSendEvent(eLoginSessionExpired);
|
||||
};
|
||||
|
||||
const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
const onAppOpenedFromURL = async (event: {
|
||||
url: string;
|
||||
isInitialUrl?: boolean;
|
||||
}) => {
|
||||
const url = event.url;
|
||||
|
||||
try {
|
||||
@@ -172,7 +177,7 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
return;
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_note")) {
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_note?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const note = await db.notes.note(id);
|
||||
@@ -184,6 +189,53 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
url.startsWith("https://app.notesnook.com/open_notebook?") &&
|
||||
!event.isInitialUrl
|
||||
) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const notebook = await db.notebooks.notebook(id);
|
||||
if (notebook) {
|
||||
Navigation.navigate("Notebook", {
|
||||
id: notebook.id,
|
||||
canGoBack: true,
|
||||
item: notebook
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
url.startsWith("https://app.notesnook.com/open_tag?") &&
|
||||
!event.isInitialUrl
|
||||
) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const tag = await db.tags.tag(id);
|
||||
if (tag) {
|
||||
Navigation.navigate("TaggedNotes", {
|
||||
type: "tag",
|
||||
id: tag.id,
|
||||
item: tag,
|
||||
canGoBack: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
url.startsWith("https://app.notesnook.com/open_color?") &&
|
||||
!event.isInitialUrl
|
||||
) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const color = await db.colors.color(id);
|
||||
if (color) {
|
||||
Navigation.navigate("ColoredNotes", {
|
||||
type: "color",
|
||||
id: color.id,
|
||||
item: color,
|
||||
canGoBack: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_reminder")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
@@ -436,6 +488,28 @@ const IsDatabaseMigrationRequired = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
let initialDate = -1;
|
||||
async function expiringNotesTimer() {
|
||||
if (timer != null) clearInterval(timer);
|
||||
|
||||
if (initialDate === -1) {
|
||||
DatabaseLogger.info("Deleting expired notes at startup");
|
||||
db.notes.deleteExpiredNotes();
|
||||
initialDate = dayjs().date();
|
||||
}
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (dayjs().date() != initialDate) {
|
||||
DatabaseLogger.info("Deleting expired notes");
|
||||
db.notes.deleteExpiredNotes();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useRelationStore.getState().update();
|
||||
initialDate = dayjs().date();
|
||||
}
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
const initializeDatabase = async (password?: string) => {
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
@@ -455,16 +529,21 @@ const initializeDatabase = async (password?: string) => {
|
||||
if (db.isInitialized) {
|
||||
await setAppMessage();
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
useSettingStore.getState().refresh();
|
||||
Notifications.setupReminders(true);
|
||||
DatabaseLogger.info("Database initialized");
|
||||
Notifications.restorePinnedNotes();
|
||||
expiringNotesTimer();
|
||||
deleteDCacheFiles();
|
||||
}
|
||||
Walkthrough.init();
|
||||
};
|
||||
|
||||
export const useAppEvents = () => {
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
const [isAppLoading, initialUrl] = useSettingStore((state) => [
|
||||
state.isAppLoading,
|
||||
state.initialUrl
|
||||
]);
|
||||
const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [
|
||||
state.setLastSynced,
|
||||
state.setUser,
|
||||
@@ -509,6 +588,15 @@ export const useAppEvents = () => {
|
||||
};
|
||||
}, [isAppLoading, onSyncComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialUrl) {
|
||||
onAppOpenedFromURL({
|
||||
url: initialUrl!,
|
||||
isInitialUrl: true
|
||||
});
|
||||
}
|
||||
}, [initialUrl]);
|
||||
|
||||
const subscribeToPurchaseListeners = useCallback(async () => {
|
||||
if (Platform.OS === "android") {
|
||||
try {
|
||||
@@ -637,10 +725,10 @@ export const useAppEvents = () => {
|
||||
EV.subscribe(EVENTS.syncCheckStatus, onCheckSyncStatus),
|
||||
EV.subscribe(EVENTS.syncAborted, onSyncAborted),
|
||||
EV.subscribe(EVENTS.appRefreshRequested, onSyncComplete),
|
||||
EV.subscribe(EVENTS.userLoggedOut, onLogout),
|
||||
EV.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
|
||||
db.eventManager.subscribe(EVENTS.userLoggedOut, onLogout),
|
||||
db.eventManager.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
|
||||
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
|
||||
EV.subscribe(
|
||||
db.eventManager.subscribe(
|
||||
EVENTS.userSubscriptionUpdated,
|
||||
onUserSubscriptionStatusChanged
|
||||
),
|
||||
@@ -724,6 +812,7 @@ export const useAppEvents = () => {
|
||||
if (state === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
updateStatusBarColor();
|
||||
expiringNotesTimer();
|
||||
checkAutoBackup();
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
@@ -774,23 +863,10 @@ export const useAppEvents = () => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!refValues.current.initialUrl) {
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url) {
|
||||
refValues.current.initialUrl = url;
|
||||
}
|
||||
});
|
||||
}
|
||||
let sub: NativeEventSubscription;
|
||||
if (!isAppLoading && !appLocked) {
|
||||
setTimeout(() => {
|
||||
sub = AppState.addEventListener("change", onAppStateChanged);
|
||||
if (refValues.current.initialUrl) {
|
||||
onAppOpenedFromURL({
|
||||
url: refValues.current.initialUrl!
|
||||
});
|
||||
refValues.current.initialUrl = undefined;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
refValues.current.removeInternetStateListener = NetInfo.addEventListener(
|
||||
|
||||
@@ -31,8 +31,6 @@ import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { rootNavigatorRef } from "../utils/global-refs";
|
||||
import Navigation from "../services/navigation";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { useStoredValue } from "../hooks/use-stored-state";
|
||||
import { db } from "../common/database";
|
||||
|
||||
const RootStack = createNativeStackNavigator();
|
||||
const AppStack = createNativeStackNavigator();
|
||||
@@ -58,105 +56,117 @@ const AppNavigation = React.memo(
|
||||
() => {
|
||||
const { colors } = useThemeColors();
|
||||
const homepageV2 = useSettingStore((state) => state.settings.homepageV2);
|
||||
const home = useStoredValue(
|
||||
"app-home-navigtion-key",
|
||||
!homepageV2 || homepageV2.id === DEFAULT_HOME.name
|
||||
? DEFAULT_HOME
|
||||
: undefined
|
||||
);
|
||||
const loading = useSettingStore((state) => state.isAppLoading);
|
||||
const [home, setHome] = React.useState<
|
||||
{ name: string; params: any } | undefined
|
||||
>(undefined);
|
||||
|
||||
if (!home.value && !homepageV2) {
|
||||
home.value = DEFAULT_HOME;
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (!home) {
|
||||
if (useSettingStore.getState().initialUrl) {
|
||||
const url = useSettingStore.getState().initialUrl;
|
||||
if (url?.startsWith("https://app.notesnook.com/open_notebook?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
setHome({
|
||||
name: "Notebook",
|
||||
params: {
|
||||
id: id
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (url?.startsWith("https://app.notesnook.com/open_tag?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
setHome({
|
||||
name: "TaggedNotes",
|
||||
params: {
|
||||
type: "tag",
|
||||
id: id
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (url?.startsWith("https://app.notesnook.com/open_color?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
setHome({
|
||||
name: "ColoredNotes",
|
||||
params: {
|
||||
type: "color",
|
||||
id: id
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (homepageV2) {
|
||||
switch (homepageV2.type) {
|
||||
case "notebook": {
|
||||
setHome({
|
||||
name: "Notebook",
|
||||
params: {
|
||||
id: homepageV2.id
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "color": {
|
||||
setHome({
|
||||
name: "ColoredNotes",
|
||||
params: {
|
||||
type: "color",
|
||||
id: homepageV2.id
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "tag": {
|
||||
setHome({
|
||||
name: "TaggedNotes",
|
||||
params: {
|
||||
type: "tag",
|
||||
id: homepageV2.id
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
case "default":
|
||||
setHome({
|
||||
name: homepageV2.id,
|
||||
params: undefined
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setHome(DEFAULT_HOME);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!homepageV2 || loading) return;
|
||||
(async () => {
|
||||
isFeatureAvailable("customHomepage").then((value) => {
|
||||
if (!value.isAllowed) {
|
||||
home.value = DEFAULT_HOME;
|
||||
SettingsService.setProperty("homepageV2", undefined);
|
||||
}
|
||||
});
|
||||
|
||||
if (!home.value) {
|
||||
switch (homepageV2.type) {
|
||||
case "notebook":
|
||||
{
|
||||
const notebook = await db.notebooks.notebook(homepageV2.id);
|
||||
if (notebook) {
|
||||
home.value = {
|
||||
name: "Notebook",
|
||||
params: {
|
||||
item: notebook,
|
||||
id: notebook.id,
|
||||
title: notebook.title
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "color": {
|
||||
const color = await db.colors.color(homepageV2.id);
|
||||
if (color) {
|
||||
home.value = {
|
||||
name: "ColoredNotes",
|
||||
params: {
|
||||
item: color,
|
||||
id: color.id,
|
||||
title: color.title
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "tag": {
|
||||
const tag = await db.tags.tag(homepageV2.id);
|
||||
if (tag) {
|
||||
home.value = {
|
||||
name: "TaggedNotes",
|
||||
params: {
|
||||
item: tag,
|
||||
id: tag.id,
|
||||
title: tag.title
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "default":
|
||||
{
|
||||
home.value = DEFAULT_HOME;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
home.value = undefined;
|
||||
setTimeout(() => {
|
||||
home.value = DEFAULT_HOME;
|
||||
});
|
||||
isFeatureAvailable("customHomepage").then((value) => {
|
||||
if (!value.isAllowed) {
|
||||
SettingsService.setProperty("homepageV2", undefined);
|
||||
}
|
||||
})();
|
||||
}, [homepageV2, loading, home.value]);
|
||||
});
|
||||
}, [homepageV2, loading]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!home) return;
|
||||
useNavigationStore.getState().update(home?.name as keyof RouteParams);
|
||||
useNavigationStore
|
||||
.getState()
|
||||
.update(home.value?.name as keyof RouteParams);
|
||||
useNavigationStore
|
||||
.getState()
|
||||
.setFocusedRouteId(home.value?.params?.id || home.value?.name);
|
||||
.setFocusedRouteId(home?.params?.id || home?.name);
|
||||
}, [home]);
|
||||
|
||||
return !home.value ? null : (
|
||||
return !home ? null : (
|
||||
<AppStack.Navigator
|
||||
initialRouteName={home.value?.name}
|
||||
initialRouteName={home?.name}
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
animation: "none",
|
||||
@@ -197,7 +207,7 @@ const AppNavigation = React.memo(
|
||||
return TaggedNotes;
|
||||
}}
|
||||
initialParams={
|
||||
home.value?.name === "TaggedNotes" ? home.value?.params : undefined
|
||||
home?.name === "TaggedNotes" ? home?.params : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -209,7 +219,7 @@ const AppNavigation = React.memo(
|
||||
return ColoredNotes;
|
||||
}}
|
||||
initialParams={
|
||||
home.value?.name === "ColoredNotes" ? home.value?.params : undefined
|
||||
home?.name === "ColoredNotes" ? home?.params : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -244,9 +254,7 @@ const AppNavigation = React.memo(
|
||||
Notebook = Notebook || require("../screens/notebook").default;
|
||||
return Notebook;
|
||||
}}
|
||||
initialParams={
|
||||
home.value?.name === "Notebook" ? home.value?.params : undefined
|
||||
}
|
||||
initialParams={home?.name === "Notebook" ? home?.params : undefined}
|
||||
/>
|
||||
|
||||
<AppStack.Screen
|
||||
|
||||
@@ -42,9 +42,9 @@ import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation, { NavigationProps } from "../../services/navigation";
|
||||
import Notifications from "../../services/notifications";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useRelationStore } from "../../stores/use-relation-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
|
||||
@@ -70,17 +70,9 @@ const RecurringModes = {
|
||||
Year: "year"
|
||||
};
|
||||
|
||||
const WeekDays = new Array(7).fill(true);
|
||||
const WeekDays = [0, 1, 2, 3, 4, 5, 6];
|
||||
const WeekDaysMon = [1, 2, 3, 4, 5, 6, 0];
|
||||
const MonthDays = new Array(31).fill(true);
|
||||
const WeekDayNames = {
|
||||
0: "Sunday",
|
||||
1: "Monday",
|
||||
2: "Tuesday",
|
||||
3: "Wednesday",
|
||||
4: "Thursday",
|
||||
5: "Friday",
|
||||
6: "Saturday"
|
||||
};
|
||||
|
||||
const ReminderNotificationModes = {
|
||||
Silent: "silent",
|
||||
@@ -92,6 +84,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
const { reminder, reference } = props.route.params;
|
||||
useNavigationFocus(props.navigation, { focusOnInit: true });
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const weekFormat = useSettingStore((state) => state.weekFormat);
|
||||
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
|
||||
reminder?.mode || "once"
|
||||
);
|
||||
@@ -151,7 +144,8 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
const isSecondLast = index === selectedDays.length - 2;
|
||||
const joinWith = isSecondLast ? " & " : isLast ? "" : ", ";
|
||||
return recurringMode === RecurringModes.Week
|
||||
? WeekDayNames[day as keyof typeof WeekDayNames] + joinWith
|
||||
? strings.weekDayNames[day as keyof typeof strings.weekDayNames]() +
|
||||
joinWith
|
||||
: `${day}${nth(day)} ${joinWith}`;
|
||||
})
|
||||
.join("");
|
||||
@@ -390,37 +384,39 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
recurringMode === RecurringModes.Year
|
||||
? null
|
||||
: recurringMode === RecurringModes.Week
|
||||
? WeekDays.map((item, index) => (
|
||||
<Button
|
||||
key={strings.weekDayNamesShort[
|
||||
index as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
title={strings.weekDayNamesShort[
|
||||
index as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
type={
|
||||
selectedDays.indexOf(index) > -1
|
||||
? "selected"
|
||||
: "plain"
|
||||
}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
setSelectedDays((days) => {
|
||||
if (days.indexOf(index) > -1) {
|
||||
days.splice(days.indexOf(index), 1);
|
||||
? (weekFormat === "Mon" ? WeekDaysMon : WeekDays).map(
|
||||
(item) => (
|
||||
<Button
|
||||
key={strings.weekDayNamesShort[
|
||||
item as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
title={strings.weekDayNamesShort[
|
||||
item as keyof typeof strings.weekDayNamesShort
|
||||
]()}
|
||||
type={
|
||||
selectedDays.indexOf(item) > -1
|
||||
? "selected"
|
||||
: "plain"
|
||||
}
|
||||
fontSize={AppFontSize.xs}
|
||||
style={{
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
marginRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
setSelectedDays((days) => {
|
||||
if (days.indexOf(item) > -1) {
|
||||
days.splice(days.indexOf(item), 1);
|
||||
return [...days];
|
||||
}
|
||||
days.push(item);
|
||||
return [...days];
|
||||
}
|
||||
days.push(index);
|
||||
return [...days];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)
|
||||
: MonthDays.map((item, index) => (
|
||||
<Button
|
||||
key={index + "monthday"}
|
||||
@@ -468,6 +464,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={hideDatePicker}
|
||||
isDarkModeEnabled={isDark}
|
||||
firstDayOfWeek={weekFormat === "Mon" ? 1 : 0}
|
||||
is24Hour={db.settings.getTimeFormat() === "24-hour"}
|
||||
date={date || new Date(Date.now())}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,8 @@ import React, {
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { Platform, ViewStyle } from "react-native";
|
||||
import WebView from "react-native-webview";
|
||||
@@ -109,17 +110,20 @@ const Editor = React.memo(
|
||||
noToolbar,
|
||||
noHeader
|
||||
});
|
||||
const renderKey = useRef(randId("editor-id") + editorId);
|
||||
const [renderKey, setRenderKey] = useState(
|
||||
randId("editor-id") + editorId
|
||||
);
|
||||
useImperativeHandle(ref, () => ({
|
||||
get: () => editor
|
||||
}));
|
||||
useLockedNoteHandler();
|
||||
|
||||
const onError = useCallback(() => {
|
||||
renderKey.current = randId("editor-id") + editorId;
|
||||
setRenderKey(randId("editor-id") + editorId);
|
||||
editor.state.current.ready = false;
|
||||
editor.state.current.initialLoadCalled = false;
|
||||
editor.setLoading(true);
|
||||
}, [editor, editorId]);
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = [eSubscribeEvent(eEditorReset, onError)];
|
||||
@@ -144,7 +148,7 @@ const Editor = React.memo(
|
||||
<WebView
|
||||
testID={notesnook.editor.id}
|
||||
ref={editor.ref}
|
||||
key={renderKey.current}
|
||||
key={renderKey}
|
||||
onRenderProcessGone={onError}
|
||||
nestedScrollEnabled
|
||||
onError={onError}
|
||||
|
||||
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import type { ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import { useEditor } from "./use-editor";
|
||||
import { FeatureId, FeatureResult } from "@notesnook/common";
|
||||
import { DayFormat } from "@notesnook/core";
|
||||
export type useEditorType = ReturnType<typeof useEditor>;
|
||||
|
||||
export type EditorState = {
|
||||
@@ -60,6 +61,8 @@ export type Settings = {
|
||||
markdownShortcuts: boolean;
|
||||
features: Record<any, any>;
|
||||
loggedIn: boolean;
|
||||
defaultLineHeight: number;
|
||||
dayFormat: DayFormat;
|
||||
};
|
||||
|
||||
export type EditorProps = {
|
||||
|
||||
@@ -28,11 +28,13 @@ import { getDefaultPresets } from "@notesnook/editor/dist/cjs/toolbar/tool-defin
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import {
|
||||
BackHandler,
|
||||
Keyboard,
|
||||
KeyboardEventListener,
|
||||
NativeEventSubscription,
|
||||
Platform,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import { WebViewMessageEvent } from "react-native-webview";
|
||||
@@ -82,6 +84,9 @@ import { useDragState } from "../../settings/editor/state";
|
||||
import { EditorMessage, EditorProps, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { editorState, openInternalLink } from "./utils";
|
||||
import filesystem from "../../../common/filesystem";
|
||||
import ReactNativeBlobUtil from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../../components/sheets/export-notes/share";
|
||||
|
||||
const publishNote = async () => {
|
||||
const user = useUserStore.getState().user;
|
||||
@@ -153,16 +158,22 @@ export const useEditorEvents = (
|
||||
const features = useAreFeaturesAvailable([
|
||||
"callout",
|
||||
"outlineList",
|
||||
"taskList"
|
||||
"taskList",
|
||||
"importCsvToTable",
|
||||
"exportTableAsCsv"
|
||||
]);
|
||||
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||
const corsProxy = useSettingStore((state) => state.settings.corsProxy);
|
||||
const loading = useSettingStore((state) => state.isAppLoading);
|
||||
const [dateFormat, timeFormat] = useSettingStore((state) => [
|
||||
state.dateFormat,
|
||||
state.timeFormat
|
||||
]);
|
||||
const [dateFormat, timeFormat, defaultLineHeight, dayFormat] =
|
||||
useSettingStore((state) => [
|
||||
state.dateFormat,
|
||||
state.timeFormat,
|
||||
state.settings.defaultLineHeight,
|
||||
state.dayFormat
|
||||
]);
|
||||
const handleBack = useRef<NativeEventSubscription>(undefined);
|
||||
const loggedIn = useUserStore((state) => !!state.user);
|
||||
const { fontScale } = useWindowDimensions();
|
||||
@@ -222,10 +233,12 @@ export const useEditorEvents = (
|
||||
fontFamily: SettingsService.get().defaultFontFamily,
|
||||
dateFormat: db.settings?.getDateFormat(),
|
||||
timeFormat: db.settings?.getTimeFormat(),
|
||||
dayFormat: db.settings?.getDayFormat(),
|
||||
fontScale,
|
||||
markdownShortcuts,
|
||||
features,
|
||||
loggedIn
|
||||
loggedIn,
|
||||
defaultLineHeight
|
||||
});
|
||||
}, [
|
||||
fullscreen,
|
||||
@@ -241,10 +254,13 @@ export const useEditorEvents = (
|
||||
defaultFontFamily,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dayFormat,
|
||||
loading,
|
||||
fontScale,
|
||||
markdownShortcuts,
|
||||
loggedIn
|
||||
loggedIn,
|
||||
defaultLineHeight,
|
||||
features
|
||||
]);
|
||||
|
||||
const onBackPress = useCallback(async () => {
|
||||
@@ -733,6 +749,62 @@ export const useEditorEvents = (
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EditorEvents.downloadCsv: {
|
||||
try {
|
||||
const csv = editorMessage.value;
|
||||
let filePath: string;
|
||||
let fileName: string;
|
||||
if (Platform.OS === "android") {
|
||||
let file = await ScopedStorage.createDocument(
|
||||
"table.csv",
|
||||
"text/csv",
|
||||
csv,
|
||||
"utf8"
|
||||
);
|
||||
if (!file) return;
|
||||
filePath = file.path || file.uri;
|
||||
fileName = file.name;
|
||||
} else {
|
||||
const path = await filesystem.checkAndCreateDir("/");
|
||||
let possibleFileName = "table.csv";
|
||||
let counter = 1;
|
||||
|
||||
// Check if file exists and find available filename
|
||||
while (
|
||||
await ReactNativeBlobUtil.fs.exists(path + possibleFileName)
|
||||
) {
|
||||
possibleFileName = `table (${counter}).csv`;
|
||||
counter++;
|
||||
}
|
||||
await ReactNativeBlobUtil.fs.writeFile(
|
||||
path + possibleFileName,
|
||||
csv,
|
||||
"utf8"
|
||||
);
|
||||
filePath = path + possibleFileName;
|
||||
fileName = possibleFileName;
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
presentSheet({
|
||||
title: "Table saved to csv",
|
||||
paragraph: strings.fileSaved(fileName, Platform.OS),
|
||||
icon: "download",
|
||||
context: "global",
|
||||
component: (
|
||||
<ShareComponent uri={filePath} name={fileName} padding={12} />
|
||||
)
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message: "Could not save table to csv"
|
||||
});
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -298,7 +298,7 @@ export const useEditor = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (isContentInvalid(data) && id) {
|
||||
if (!title && isContentInvalid(data) && id) {
|
||||
// Create a new history session if recieved empty or invalid content
|
||||
// To ensure that history is preserved for correct content.
|
||||
currentSessionHistoryId = editorSessionHistory.newSession(id);
|
||||
@@ -684,8 +684,6 @@ export const useEditor = (
|
||||
lastContentChangeTime.current[item.id] = item.dateEdited;
|
||||
currentNotes.current[item.id] = item;
|
||||
|
||||
if (!currentNotes.current[item.id]) return;
|
||||
|
||||
editorSessionHistory.newSession(item.id);
|
||||
|
||||
await commands.setStatus(
|
||||
@@ -1097,10 +1095,15 @@ export const useEditor = (
|
||||
|
||||
if (!state.current?.initialLoadCalled) {
|
||||
const url = await Linking.getInitialURL();
|
||||
let noteId = url && new URL(url).searchParams.get("id");
|
||||
let noteId =
|
||||
url &&
|
||||
url.startsWith("https://app.notesnook.com/open_note?") &&
|
||||
new URL(url).searchParams.get("id");
|
||||
if (noteId) {
|
||||
const note = await db.notes?.note(noteId);
|
||||
fluidTabsRef.current?.goToPage("editor");
|
||||
loadNoteMutex.cancel();
|
||||
loadNoteMutex.release();
|
||||
if (note) {
|
||||
loadNote({
|
||||
item: note
|
||||
@@ -1108,6 +1111,8 @@ export const useEditor = (
|
||||
}
|
||||
} else {
|
||||
noteId = useTabStore.getState().getCurrentNoteId() || null;
|
||||
loadNoteMutex.cancel();
|
||||
loadNoteMutex.release();
|
||||
if (!noteId) {
|
||||
loadNote({ newNote: true });
|
||||
if (fluidTabsRef.current?.page() === "editor") {
|
||||
|
||||
@@ -38,7 +38,6 @@ import { eUpdateNotebookRoute } from "../../utils/events";
|
||||
import { findRootNotebookId } from "../../utils/notebooks";
|
||||
import { openEditor, setOnFirstSave } from "../notes/common";
|
||||
import { View } from "react-native";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { Notebooks } from "../../components/sheets/notebooks";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { rootNavigatorRef } from "../../utils/global-refs";
|
||||
@@ -47,6 +46,9 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
|
||||
const params = useRef<NotebookScreenParams>(route?.params);
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
const [notebook, setNotebook] = useState<Notebook | undefined>(
|
||||
params.current.item
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const updateOnFocus = useRef(false);
|
||||
const [breadcrumbs, setBreadcrumbs] = useState<
|
||||
@@ -75,10 +77,10 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
});
|
||||
|
||||
const syncWithNavigation = React.useCallback(() => {
|
||||
useNavigationStore.getState().setFocusedRouteId(params?.current?.item?.id);
|
||||
useNavigationStore.getState().setFocusedRouteId(params?.current?.id);
|
||||
setOnFirstSave({
|
||||
type: "notebook",
|
||||
id: params.current.item.id
|
||||
id: params.current.id
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -86,23 +88,20 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
async (data?: NotebookScreenParams) => {
|
||||
if (useSettingStore.getState().isAppLoading) return;
|
||||
if (
|
||||
useNavigationStore.getState().focusedRouteId !==
|
||||
params.current.item.id &&
|
||||
useNavigationStore.getState().focusedRouteId !== params.current.id &&
|
||||
!data
|
||||
) {
|
||||
updateOnFocus.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.item?.id && params.current.item?.id !== data?.item?.id) {
|
||||
const nextRootNotebookId = await findRootNotebookId(data?.item?.id);
|
||||
const currentNotebookRoot = await findRootNotebookId(
|
||||
params.current.item.id
|
||||
);
|
||||
if (data?.id && params.current?.id !== data?.id) {
|
||||
const nextRootNotebookId = await findRootNotebookId(data?.id);
|
||||
const currentNotebookRoot = await findRootNotebookId(params.current.id);
|
||||
|
||||
if (
|
||||
nextRootNotebookId !== currentNotebookRoot ||
|
||||
nextRootNotebookId === params.current?.item?.id
|
||||
nextRootNotebookId === params.current?.id
|
||||
) {
|
||||
// Never update notebook in route if root is different or if the root is current notebook.
|
||||
return;
|
||||
@@ -112,14 +111,13 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
if (data) params.current = data;
|
||||
|
||||
try {
|
||||
const notebook = await db.notebooks?.notebook(
|
||||
params?.current?.item?.id
|
||||
);
|
||||
|
||||
const notebook = await db.notebooks?.notebook(params?.current?.id);
|
||||
setNotebook(notebook);
|
||||
params.current.item = notebook;
|
||||
if (notebook) {
|
||||
const breadcrumbs = await db.notebooks.breadcrumbs(notebook.id);
|
||||
setBreadcrumbs(breadcrumbs.slice(0, breadcrumbs.length - 1));
|
||||
params.current.item = notebook;
|
||||
params.current.id = notebook.id;
|
||||
const notes = await db.relations
|
||||
.from(notebook, "note")
|
||||
.selector.grouped(db.settings.getGroupOptions("notes"));
|
||||
@@ -160,29 +158,27 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
<>
|
||||
<Header
|
||||
renderedInRoute={route.name}
|
||||
title={params.current.item?.title}
|
||||
title={notebook?.title}
|
||||
canGoBack={params?.current?.canGoBack}
|
||||
rightButton={{
|
||||
name: "dots-vertical",
|
||||
onPress: () => {
|
||||
Properties.present(params.current.item);
|
||||
Properties.present(notebook);
|
||||
}
|
||||
}}
|
||||
hasSearch={true}
|
||||
onSearch={() => {
|
||||
const selector = db.relations.from(
|
||||
params.current.item,
|
||||
"note"
|
||||
).selector;
|
||||
if (!notebook) return;
|
||||
const selector = db.relations.from(notebook, "note").selector;
|
||||
Navigation.push("Search", {
|
||||
placeholder: strings.searchInRoute(params.current.item?.title),
|
||||
placeholder: strings.searchInRoute(notebook?.title),
|
||||
type: "note",
|
||||
title: params.current.item?.title,
|
||||
title: notebook?.title,
|
||||
route: route.name,
|
||||
items: selector
|
||||
});
|
||||
}}
|
||||
id={params.current.item?.id}
|
||||
id={notebook?.id}
|
||||
/>
|
||||
|
||||
<DelayLayout wait={loading}>
|
||||
@@ -192,19 +188,21 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
onRefresh={() => {
|
||||
onRequestUpdate();
|
||||
}}
|
||||
id={params.current.item?.id}
|
||||
id={params.current?.id}
|
||||
renderedInRoute="Notebook"
|
||||
headerTitle={params.current.item.title}
|
||||
headerTitle={notebook?.title}
|
||||
loading={loading}
|
||||
CustomLisHeader={
|
||||
<NotebookHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
notebook={params.current.item}
|
||||
totalNotes={notes?.placeholders.length || 0}
|
||||
/>
|
||||
notebook ? (
|
||||
<NotebookHeader
|
||||
breadcrumbs={breadcrumbs}
|
||||
notebook={notebook}
|
||||
totalNotes={notes?.placeholders.length || 0}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
placeholder={{
|
||||
title: params.current.item?.title,
|
||||
title: notebook?.title!,
|
||||
paragraph: strings.notesEmpty(),
|
||||
button: strings.addFirstNote(),
|
||||
action: openEditor,
|
||||
@@ -225,7 +223,8 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
testID="notebookTreeSheet"
|
||||
size="small"
|
||||
onPress={() => {
|
||||
Notebooks.present(params.current.item);
|
||||
if (!notebook) return;
|
||||
Notebooks.present(notebook);
|
||||
}}
|
||||
style={{
|
||||
position: "relative",
|
||||
@@ -246,7 +245,7 @@ const NotebookScreen = ({ route, navigation }: NavigationProps<"Notebook">) => {
|
||||
/>
|
||||
</View>
|
||||
<SelectionHeader
|
||||
id={route.params?.item?.id}
|
||||
id={route.params?.id}
|
||||
items={notes}
|
||||
type="note"
|
||||
renderedInRoute="Notebook"
|
||||
@@ -260,6 +259,7 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
|
||||
const { currentRoute, focusedRouteId } = useNavigationStore.getState();
|
||||
if (currentRoute === "Notebooks") {
|
||||
Navigation.push("Notebook", {
|
||||
id: item.id,
|
||||
item: item,
|
||||
canGoBack
|
||||
});
|
||||
@@ -276,22 +276,24 @@ NotebookScreen.navigate = async (item: Notebook, canGoBack?: boolean) => {
|
||||
// Update the route in place instead
|
||||
|
||||
eSendEvent(eUpdateNotebookRoute, {
|
||||
item: item,
|
||||
title: item.title,
|
||||
canGoBack: canGoBack
|
||||
id: item.id,
|
||||
canGoBack: canGoBack,
|
||||
item: item
|
||||
});
|
||||
} else {
|
||||
// Push a new route
|
||||
Navigation.push("Notebook", {
|
||||
item: item,
|
||||
canGoBack
|
||||
id: item.id,
|
||||
canGoBack,
|
||||
item: item
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Push a new route anyways
|
||||
Navigation.push("Notebook", {
|
||||
item: item,
|
||||
canGoBack
|
||||
id: item.id,
|
||||
canGoBack,
|
||||
item: item
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ import Navigation, { NavigationProps } from "../../services/navigation";
|
||||
import useNavigationStore, {
|
||||
NotesScreenParams
|
||||
} from "../../stores/use-navigation-store";
|
||||
import { PLACEHOLDER_DATA, openEditor, toCamelCase } from "./common";
|
||||
import { PLACEHOLDER_DATA, openEditor } from "./common";
|
||||
export const ColoredNotes = ({
|
||||
navigation,
|
||||
route
|
||||
@@ -45,11 +45,13 @@ export const ColoredNotes = ({
|
||||
|
||||
ColoredNotes.get = async (params: NotesScreenParams, grouped = true) => {
|
||||
if (!grouped) {
|
||||
return await db.relations.from(params.item, "note").resolve();
|
||||
return await db.relations
|
||||
.from({ id: params.id, type: "color" }, "note")
|
||||
.resolve();
|
||||
}
|
||||
|
||||
return await db.relations
|
||||
.from(params.item, "note")
|
||||
.from({ id: params.id, type: "color" }, "note")
|
||||
.selector.grouped(db.settings.getGroupOptions("notes"));
|
||||
};
|
||||
|
||||
@@ -63,8 +65,10 @@ ColoredNotes.navigate = (item: Color, canGoBack: boolean) => {
|
||||
}
|
||||
|
||||
Navigation.push<"ColoredNotes">("ColoredNotes", {
|
||||
item: item,
|
||||
canGoBack
|
||||
type: "color",
|
||||
id: item.id,
|
||||
canGoBack,
|
||||
item: item
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { resolveItems } from "@notesnook/common";
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Tag, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Color, Note } from "@notesnook/core";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { db } from "../../common/database";
|
||||
@@ -76,14 +76,12 @@ const NotesPage = ({
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
|
||||
const [loadingNotes, setLoadingNotes] = useState(true);
|
||||
const isMonograph = route.name === "Monographs";
|
||||
const title =
|
||||
params.current?.item.type === "tag"
|
||||
? "#" + params.current?.item.title
|
||||
: params.current?.item.title;
|
||||
const [item, setItem] = useState<Tag | Color | undefined>(
|
||||
params.current.item
|
||||
);
|
||||
const title = item?.type === "tag" ? "#" + item?.title : item?.title;
|
||||
const accentColor =
|
||||
route.name === "ColoredNotes"
|
||||
? (params.current?.item as Color)?.colorCode
|
||||
: undefined;
|
||||
route.name === "ColoredNotes" ? (item as Color)?.colorCode : undefined;
|
||||
const updateOnFocus = useRef(false);
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
const isFocused = useNavigationFocus(navigation, {
|
||||
@@ -107,31 +105,30 @@ const NotesPage = ({
|
||||
});
|
||||
|
||||
const syncWithNavigation = React.useCallback(() => {
|
||||
const { item } = params.current;
|
||||
useNavigationStore
|
||||
.getState()
|
||||
.setFocusedRouteId(params?.current?.item?.id || route.name);
|
||||
const { id } = params.current;
|
||||
useNavigationStore.getState().setFocusedRouteId(id || route.name);
|
||||
|
||||
!isMonograph &&
|
||||
setOnFirstSave({
|
||||
type: getItemType(route.name),
|
||||
id: item.id
|
||||
id: id
|
||||
});
|
||||
}, [isMonograph, route.name]);
|
||||
|
||||
const onRequestUpdate = React.useCallback(
|
||||
async (data?: NotesScreenParams) => {
|
||||
if (useSettingStore.getState().isAppLoading) return;
|
||||
|
||||
if (
|
||||
params.current.item.id &&
|
||||
useNavigationStore.getState().focusedRouteId !==
|
||||
params.current.item.id &&
|
||||
params.current.id &&
|
||||
useNavigationStore.getState().focusedRouteId !== params.current.id &&
|
||||
!data
|
||||
) {
|
||||
updateOnFocus.current = false;
|
||||
return;
|
||||
}
|
||||
const isNew = data && data?.item?.id !== params.current?.item?.id;
|
||||
|
||||
const isNew = data && data?.id !== params.current?.id;
|
||||
if (data) params.current = data;
|
||||
|
||||
try {
|
||||
@@ -142,9 +139,9 @@ const NotesPage = ({
|
||||
)) as VirtualizedGrouping<Note>;
|
||||
|
||||
if (route.name === "TaggedNotes" || route.name === "ColoredNotes") {
|
||||
const item = await (db as any)[params.current.item.type + "s"][
|
||||
params.current.item.type
|
||||
](params.current.item.id);
|
||||
const item = await (db as any)[params.current.type + "s"][
|
||||
params.current.type
|
||||
](params.current.id);
|
||||
|
||||
if (!item) {
|
||||
if (rootNavigatorRef.canGoBack()) {
|
||||
@@ -154,7 +151,7 @@ const NotesPage = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setItem(item);
|
||||
params.current.item = item;
|
||||
}
|
||||
|
||||
@@ -173,15 +170,7 @@ const NotesPage = ({
|
||||
useEffect(() => {
|
||||
if (isAppLoading) return;
|
||||
if (loadingNotes) {
|
||||
get(params.current, true)
|
||||
.then(async (items) => {
|
||||
setNotes(items as VirtualizedGrouping<Note>);
|
||||
await (items as VirtualizedGrouping<Note>).item(0, resolveItems);
|
||||
setLoadingNotes(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setLoadingNotes(false);
|
||||
});
|
||||
onRequestUpdate(params.current);
|
||||
}
|
||||
}, [loadingNotes, get, isAppLoading]);
|
||||
|
||||
@@ -202,19 +191,18 @@ const NotesPage = ({
|
||||
}
|
||||
canGoBack={params?.current?.canGoBack}
|
||||
hasSearch={true}
|
||||
id={
|
||||
route.name === "Monographs" ? "Monographs" : params?.current.item?.id
|
||||
}
|
||||
id={route.name === "Monographs" ? "Monographs" : params?.current?.id}
|
||||
onSearch={() => {
|
||||
if (!item) return;
|
||||
const selector =
|
||||
route.name === "Monographs"
|
||||
? db.monographs.all
|
||||
: db.relations.from(params.current.item, "note").selector;
|
||||
: db.relations.from(item, "note").selector;
|
||||
|
||||
Navigation.push("Search", {
|
||||
placeholder: strings.searchInRoute(title || route.name),
|
||||
type: "note",
|
||||
title: title,
|
||||
title: title!,
|
||||
route: route.name,
|
||||
items: selector
|
||||
});
|
||||
@@ -229,7 +217,7 @@ const NotesPage = ({
|
||||
onRefresh={onRequestUpdate}
|
||||
loading={false}
|
||||
renderedInRoute={route.name}
|
||||
id={params.current.item?.id}
|
||||
id={params.current?.id}
|
||||
headerTitle={title || "Monographs"}
|
||||
customAccentColor={accentColor}
|
||||
placeholder={placeholder}
|
||||
@@ -246,7 +234,7 @@ const NotesPage = ({
|
||||
) : null}
|
||||
</DelayLayout>
|
||||
<SelectionHeader
|
||||
id={route.params?.item?.id || route.name}
|
||||
id={route.params?.id || route.name}
|
||||
items={notes}
|
||||
type="note"
|
||||
renderedInRoute={route.name}
|
||||
|
||||
@@ -62,7 +62,8 @@ Monographs.get = async (params?: NotesScreenParams, grouped = true) => {
|
||||
|
||||
Monographs.navigate = (canGoBack?: boolean) => {
|
||||
Navigation.navigate<"Monographs">("Monographs", {
|
||||
item: { type: "monograph" } as any,
|
||||
type: "monograph",
|
||||
id: "monograph",
|
||||
canGoBack: canGoBack as boolean
|
||||
});
|
||||
};
|
||||
|
||||
@@ -46,11 +46,13 @@ export const TaggedNotes = ({
|
||||
|
||||
TaggedNotes.get = async (params: NotesScreenParams, grouped = true) => {
|
||||
if (!grouped) {
|
||||
return await db.relations.from(params.item, "note").resolve();
|
||||
return await db.relations
|
||||
.from({ id: params.id, type: "tag" }, "note")
|
||||
.resolve();
|
||||
}
|
||||
|
||||
return await db.relations
|
||||
.from(params.item, "note")
|
||||
.from({ id: params.id, type: "tag" }, "note")
|
||||
.selector.grouped(db.settings.getGroupOptions("notes"));
|
||||
};
|
||||
|
||||
@@ -64,8 +66,10 @@ TaggedNotes.navigate = (item: Tag, canGoBack?: boolean) => {
|
||||
}
|
||||
|
||||
Navigation.push<"TaggedNotes">("TaggedNotes", {
|
||||
item: item,
|
||||
canGoBack
|
||||
id: item.id,
|
||||
type: "tag",
|
||||
canGoBack,
|
||||
item: item
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
BackupReminderPicker,
|
||||
BackupWithAttachmentsReminderPicker,
|
||||
DateFormatPicker,
|
||||
DayFormatPicker,
|
||||
WeekFormatPicker,
|
||||
FontPicker,
|
||||
HomePicker,
|
||||
SidebarTabPicker,
|
||||
@@ -57,6 +59,8 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"title-format": <TitleFormat />,
|
||||
"date-format-selector": <DateFormatPicker />,
|
||||
"time-format-selector": <TimeFormatPicker />,
|
||||
"day-format-selector": <DayFormatPicker />,
|
||||
"week-format-selector": <WeekFormatPicker />,
|
||||
"theme-selector": <ThemeSelector />,
|
||||
"applock-timer": <ApplockTimerPicker />,
|
||||
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,
|
||||
|
||||
@@ -32,6 +32,18 @@ import { strings } from "@notesnook/intl";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import PaywallSheet from "../../../components/sheets/paywall";
|
||||
|
||||
const DAY_FORMATS = ["short", "long"];
|
||||
const DayFormatFormats = {
|
||||
short: "ddd",
|
||||
long: "dddd"
|
||||
};
|
||||
|
||||
const WEEK_FORMATS = ["Sun", "Mon"];
|
||||
const WeekFormatNames = {
|
||||
Sun: "Sunday",
|
||||
Mon: "Monday"
|
||||
};
|
||||
|
||||
export const FontPicker = createSettingsPicker({
|
||||
getValue: () => useSettingStore.getState().settings.defaultFontFamily,
|
||||
updateValue: (item) => {
|
||||
@@ -111,8 +123,8 @@ export const TrashIntervalPicker = createSettingsPicker({
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 1
|
||||
? strings.reminderRecurringMode.day()
|
||||
: strings.days(item);
|
||||
? strings.reminderRecurringMode.day()
|
||||
: strings.days(item);
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 1, 7, 30, 365],
|
||||
@@ -152,6 +164,42 @@ export const DateFormatPicker = createSettingsPicker({
|
||||
isOptionAvailable: () => true
|
||||
});
|
||||
|
||||
export const DayFormatPicker = createSettingsPicker({
|
||||
getValue: () => db.settings.getDayFormat(),
|
||||
updateValue: (item) => {
|
||||
db.settings.setDayFormat(item);
|
||||
useSettingStore.setState({
|
||||
dayFormat: item
|
||||
});
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return `${strings.dayFormat()} (${dayjs().format(DayFormatFormats[item])})`;
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
options: DAY_FORMATS,
|
||||
compareValue: (current, item) => current === item,
|
||||
isFeatureAvailable: () => true,
|
||||
isOptionAvailable: () => true
|
||||
});
|
||||
|
||||
export const WeekFormatPicker = createSettingsPicker({
|
||||
getValue: () => db.settings.getWeekFormat(),
|
||||
updateValue: (item) => {
|
||||
db.settings.setWeekFormat(item);
|
||||
useSettingStore.setState({
|
||||
weekFormat: item
|
||||
});
|
||||
},
|
||||
formatValue: (item) => {
|
||||
return `${WeekFormatNames[item]}`;
|
||||
},
|
||||
getItemKey: (item) => item,
|
||||
options: WEEK_FORMATS,
|
||||
compareValue: (current, item) => current === item,
|
||||
isFeatureAvailable: () => true,
|
||||
isOptionAvailable: () => true
|
||||
});
|
||||
|
||||
const TimeFormats = {
|
||||
"12-hour": "hh:mm A",
|
||||
"24-hour": "HH:mm"
|
||||
@@ -228,10 +276,10 @@ export const ApplockTimerPicker = createSettingsPicker({
|
||||
return item === -1
|
||||
? strings.never()
|
||||
: item === 0 || item === undefined
|
||||
? strings.immediately()
|
||||
: item === 1
|
||||
? strings.minutes(1)
|
||||
: strings.minutes(item);
|
||||
? strings.immediately()
|
||||
: item === 1
|
||||
? strings.minutes(1)
|
||||
: strings.minutes(item);
|
||||
},
|
||||
getItemKey: (item) => item.toString(),
|
||||
options: [-1, 0, 1, 5, 15, 30],
|
||||
|
||||
@@ -71,6 +71,7 @@ import { verifyUser, verifyUserWithApplock } from "./functions";
|
||||
import { logoutUser } from "./logout";
|
||||
import { SettingSection } from "./types";
|
||||
import { getTimeLeft } from "./user-section";
|
||||
import { EDITOR_LINE_HEIGHT } from "../../utils/constants";
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
@@ -285,10 +286,17 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "change-password",
|
||||
name: strings.changePassword(),
|
||||
type: "screen",
|
||||
// type: "screen",
|
||||
description: strings.changePasswordDesc(),
|
||||
component: "change-password",
|
||||
icon: "form-textbox-password"
|
||||
// component: "change-password",
|
||||
icon: "form-textbox-password",
|
||||
modifer: () => {
|
||||
ToastManager.show({
|
||||
type: "info",
|
||||
message:
|
||||
"Password changing has been disabled temporarily to address some issues faced by users. It will be enabled again once the issues have resolved."
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "change-email",
|
||||
@@ -715,6 +723,22 @@ export const settingsGroups: SettingSection[] = [
|
||||
component: "date-format-selector",
|
||||
icon: "calendar-blank"
|
||||
},
|
||||
{
|
||||
id: "day-format",
|
||||
name: strings.dayFormat(),
|
||||
description: strings.dayFormatDesc(),
|
||||
type: "component",
|
||||
component: "day-format-selector",
|
||||
icon: "calendar-today"
|
||||
},
|
||||
{
|
||||
id: "week-format",
|
||||
name: strings.weekFormat(),
|
||||
description: strings.weekFormatDesc(),
|
||||
type: "component",
|
||||
component: "week-format-selector",
|
||||
icon: "calendar"
|
||||
},
|
||||
{
|
||||
id: "time-format",
|
||||
name: strings.timeFormat(),
|
||||
@@ -814,6 +838,16 @@ export const settingsGroups: SettingSection[] = [
|
||||
property: "defaultFontFamily",
|
||||
component: "font-selector"
|
||||
},
|
||||
{
|
||||
id: "default-line-height",
|
||||
name: strings.lineHeight(),
|
||||
description: strings.lineHeightDesc(),
|
||||
type: "input-selector",
|
||||
property: "defaultLineHeight",
|
||||
icon: "format-line-spacing",
|
||||
minInputValue: EDITOR_LINE_HEIGHT.MIN,
|
||||
maxInputValue: EDITOR_LINE_HEIGHT.MAX
|
||||
},
|
||||
{
|
||||
id: "title-format",
|
||||
name: strings.titleFormat(),
|
||||
@@ -895,7 +929,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => (current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
item: {},
|
||||
novault: false,
|
||||
title: strings.createVault()
|
||||
});
|
||||
@@ -909,7 +942,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () =>
|
||||
openVault({
|
||||
item: {},
|
||||
changePassword: true,
|
||||
novault: true,
|
||||
title: strings.changeVaultPassword()
|
||||
@@ -923,7 +955,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
item: {},
|
||||
clearVault: true,
|
||||
novault: true,
|
||||
title: strings.clearVault() + "?"
|
||||
@@ -938,7 +969,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
hidden: (current) => !(current as VaultStatusType)?.exists,
|
||||
modifer: () => {
|
||||
openVault({
|
||||
item: {},
|
||||
deleteVault: true,
|
||||
novault: true,
|
||||
title: strings.deleteVault() + "?"
|
||||
@@ -960,7 +990,6 @@ export const settingsGroups: SettingSection[] = [
|
||||
modifer: (current) => {
|
||||
const _current = current as VaultStatusType;
|
||||
openVault({
|
||||
item: {},
|
||||
fingerprintAccess: !_current.biometryEnrolled,
|
||||
revokeFingerprintAccess: _current.biometryEnrolled,
|
||||
novault: true,
|
||||
|
||||
@@ -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 { EventHandler, EventManager } from "@notesnook/core";
|
||||
import { EventHandler, EventManager, Note, NoteContent } from "@notesnook/core";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { RefObject } from "react";
|
||||
import { ActionSheetRef } from "react-native-actions-sheet";
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
} from "../utils/events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Vault = {
|
||||
item: unknown;
|
||||
export type Vault = {
|
||||
item: Note;
|
||||
novault: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -48,6 +48,14 @@ type Vault = {
|
||||
clearVault: boolean;
|
||||
deleteVault: boolean;
|
||||
copyNote: boolean;
|
||||
customActionTitle: string;
|
||||
customActionParagraph: string;
|
||||
onUnlock: (
|
||||
item: Note & {
|
||||
content?: NoteContent<false>;
|
||||
},
|
||||
password: string
|
||||
) => void;
|
||||
};
|
||||
|
||||
type NoteEdit = {
|
||||
|
||||
@@ -23,6 +23,7 @@ import ShareExtension, {
|
||||
import { getPreviewData } from "@flyerhq/react-native-link-preview";
|
||||
import {
|
||||
formatBytes,
|
||||
getFormattedDate,
|
||||
isFeatureAvailable,
|
||||
useIsFeatureAvailable
|
||||
} from "@notesnook/common";
|
||||
@@ -47,7 +48,6 @@ import {
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
SafeAreaProvider,
|
||||
SafeAreaView,
|
||||
useSafeAreaInsets
|
||||
} from "react-native-safe-area-context";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
@@ -70,14 +70,20 @@ import { Search } from "./search";
|
||||
import { initDatabase, useShareStore } from "./store";
|
||||
import { isTablet } from "react-native-device-info";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { DefaultAppStyles } from "../utils/styles";
|
||||
|
||||
const getLinkPreview = (url: string) => {
|
||||
return getPreviewData(url, 5000);
|
||||
};
|
||||
async function sanitizeHtml(site: string) {
|
||||
async function sanitizeHtml(site: string, title?: string) {
|
||||
try {
|
||||
let html = await fetchHandle.current?.processUrl(site);
|
||||
return html;
|
||||
return (
|
||||
html +
|
||||
"<hr/>" +
|
||||
`<p>Clipped from <a href="${site}">${title || site}</a></p>
|
||||
<p>Date clipped ${getFormattedDate(Date.now())}</p>`
|
||||
);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
@@ -145,6 +151,7 @@ const ShareView = () => {
|
||||
const noteContent = useRef<string>(undefined);
|
||||
const noteTitle = useRef<string>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingData, setLoadingData] = useState(true);
|
||||
const [loadingExtension, setLoadingExtension] = useState(true);
|
||||
const fullQualityImages = useIsFeatureAvailable("fullQualityImages");
|
||||
const [rawData, setRawData] = useState<{
|
||||
@@ -167,6 +174,11 @@ const ShareView = () => {
|
||||
globalThis["IS_SHARE_EXTENSION"] = true;
|
||||
const onKeyboardDidShow = (event: KeyboardEvent) => {
|
||||
let height = Dimensions.get("screen").height - event.endCoordinates.screenY;
|
||||
if (height < 100) {
|
||||
setKeyboardHeight(0);
|
||||
keyboardHeightRef.current = 0;
|
||||
return;
|
||||
}
|
||||
keyboardHeightRef.current = height;
|
||||
setKeyboardHeight(height);
|
||||
};
|
||||
@@ -210,7 +222,6 @@ const ShareView = () => {
|
||||
};
|
||||
|
||||
const onLoad = useCallback(() => {
|
||||
console.log(noteContent.current, "current...");
|
||||
eSendEvent(eOnLoadNote + "shareEditor", {
|
||||
id: null,
|
||||
content: {
|
||||
@@ -226,6 +237,7 @@ const ShareView = () => {
|
||||
try {
|
||||
if (noteContent.current) {
|
||||
onLoad();
|
||||
setLoadingData(false);
|
||||
return;
|
||||
}
|
||||
defaultNote.content.data = undefined;
|
||||
@@ -242,6 +254,7 @@ const ShareView = () => {
|
||||
editorRef.current?.focus();
|
||||
}, 300);
|
||||
}
|
||||
setLoadingData(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -298,7 +311,9 @@ const ShareView = () => {
|
||||
});
|
||||
onLoad();
|
||||
setNote({ ...note });
|
||||
setLoadingData(false);
|
||||
} catch (e) {
|
||||
setLoadingData(false);
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
@@ -311,6 +326,7 @@ const ShareView = () => {
|
||||
await initDatabase();
|
||||
setLoadingExtension(false);
|
||||
useShareStore.getState().restore();
|
||||
loadData(false);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
@@ -389,7 +405,7 @@ const ShareView = () => {
|
||||
if (value === 2) {
|
||||
setLoadingPage(true);
|
||||
setTimeout(async () => {
|
||||
let html = await sanitizeHtml(rawData?.value || "");
|
||||
let html = await sanitizeHtml(rawData?.value || "", note.title);
|
||||
noteContent.current = html || "";
|
||||
setLoadingPage(false);
|
||||
onLoad();
|
||||
@@ -448,11 +464,7 @@ const ShareView = () => {
|
||||
overflow: "hidden",
|
||||
paddingTop: insets.top,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom:
|
||||
Platform.OS === "android" && keyboardHeight
|
||||
? keyboardHeight - (insets.bottom || (gesturesEnabled ? 25 : 50))
|
||||
: 0
|
||||
paddingRight: insets.right
|
||||
}}
|
||||
>
|
||||
{loadingPage ? <HtmlLoadingWebViewAgent /> : null}
|
||||
@@ -500,394 +512,414 @@ const ShareView = () => {
|
||||
display: searchMode ? "none" : "flex",
|
||||
borderTopRightRadius: Platform.OS === "ios" ? 10 : 15,
|
||||
borderTopLeftRadius: Platform.OS === "ios" ? 10 : 15,
|
||||
maxHeight: Platform.OS === "android" ? undefined : "100%"
|
||||
maxHeight: Platform.OS === "android" ? undefined : "100%",
|
||||
paddingBottom:
|
||||
Platform.OS === "android" && keyboardHeight
|
||||
? keyboardHeight
|
||||
: insets.bottom
|
||||
}}
|
||||
>
|
||||
<ScrollView>
|
||||
{loadingData ? (
|
||||
<View
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 300,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
<Paragraph>Loading clip...</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView>
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
minHeight: 100,
|
||||
backgroundColor: colors.primary.background,
|
||||
overflow: "hidden"
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderBottomWidth: 1,
|
||||
paddingBottom: 12,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
paddingHorizontal: 12,
|
||||
gap: 10
|
||||
minHeight: 100,
|
||||
backgroundColor: colors.primary.background,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{appendNoteId ? (
|
||||
<Heading
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "Inter-SemiBold",
|
||||
fontSize: AppFontSize.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Save note
|
||||
</Heading>
|
||||
) : (
|
||||
<TextInput
|
||||
placeholder="Enter note title"
|
||||
ref={inputRef}
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "Inter-SemiBold",
|
||||
fontSize: AppFontSize.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
onChangeText={(value) => {
|
||||
noteTitle.current = value;
|
||||
}}
|
||||
defaultValue={noteTitle.current}
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
editorRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
title="Done"
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={onPress}
|
||||
allowFontScaling={false}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{rawFiles?.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderBottomWidth: 1,
|
||||
paddingBottom: 12,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: colors.secondary.background
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{ marginBottom: 6 }}
|
||||
color={colors.primary.paragraph}
|
||||
{appendNoteId ? (
|
||||
<Heading
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "Inter-SemiBold",
|
||||
fontSize: AppFontSize.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Save note
|
||||
</Heading>
|
||||
) : (
|
||||
<TextInput
|
||||
placeholder="Enter note title"
|
||||
ref={inputRef}
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
fontFamily: "Inter-SemiBold",
|
||||
fontSize: AppFontSize.lg,
|
||||
paddingBottom: 0,
|
||||
paddingTop: 0,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
onChangeText={(value) => {
|
||||
noteTitle.current = value;
|
||||
}}
|
||||
defaultValue={noteTitle.current}
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
editorRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
title="Done"
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={onPress}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Attaching {rawFiles.length} file(s):
|
||||
</Paragraph>
|
||||
<ScrollView horizontal>
|
||||
{rawFiles.map((item) =>
|
||||
isImage(item.type) || item.value?.endsWith(".png") ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => onRemoveFile(item)}
|
||||
key={item.name}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
Platform.OS === "android"
|
||||
? `file://${item.value}`
|
||||
: item.value
|
||||
}}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: "black",
|
||||
marginRight: 6
|
||||
}}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
key={item.name}
|
||||
onPress={() => onRemoveFile(item)}
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
alignItems: "center",
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 8,
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.icon}
|
||||
size={15}
|
||||
name="file"
|
||||
/>
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.primary.paragraph}
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
paddingRight: 8
|
||||
}}
|
||||
>
|
||||
{item.name || item.value.split("/").pop()}
|
||||
{item.size ? `(${formatBytes(item.size)})` : ""}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
allowFontScaling={false}
|
||||
{rawFiles?.length > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 6
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: colors.secondary.background
|
||||
}}
|
||||
>
|
||||
Tap to remove an attachment.
|
||||
</Paragraph>
|
||||
{rawFiles.some((item) => isImage(item.type)) ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
<Paragraph
|
||||
style={{ marginBottom: 6 }}
|
||||
color={colors.primary.paragraph}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Attaching {rawFiles.length} file(s):
|
||||
</Paragraph>
|
||||
<ScrollView horizontal>
|
||||
{rawFiles.map((item) =>
|
||||
isImage(item.type) || item.value?.endsWith(".png") ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => onRemoveFile(item)}
|
||||
key={item.name}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Image
|
||||
source={{
|
||||
uri:
|
||||
Platform.OS === "android"
|
||||
? `file://${item.value}`
|
||||
: item.value
|
||||
}}
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: "black",
|
||||
marginRight: 6
|
||||
}}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
key={item.name}
|
||||
onPress={() => onRemoveFile(item)}
|
||||
style={{
|
||||
borderRadius: defaultBorderRadius,
|
||||
backgroundColor: colors.secondary.background,
|
||||
flexDirection: "row",
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
alignItems: "center",
|
||||
paddingVertical: 5,
|
||||
paddingHorizontal: 8,
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.icon}
|
||||
size={15}
|
||||
name="file"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
size={AppFontSize.xs}
|
||||
color={colors.primary.paragraph}
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
paddingRight: 8
|
||||
}}
|
||||
>
|
||||
{item.name || item.value.split("/").pop()}
|
||||
{item.size ? `(${formatBytes(item.size)})` : ""}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={AppFontSize.xs}
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
onPress={() => {
|
||||
if (fullQualityImages?.isAllowed) {
|
||||
setCompress(!compress);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={20}
|
||||
name={
|
||||
compress
|
||||
? "checkbox-marked"
|
||||
: "checkbox-blank-outline"
|
||||
}
|
||||
allowFontScaling={false}
|
||||
color={
|
||||
compress && fullQualityImages?.isAllowed
|
||||
? colors.primary.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Text
|
||||
Tap to remove an attachment.
|
||||
</Paragraph>
|
||||
{rawFiles.some((item) => isImage(item.type)) ? (
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 3,
|
||||
fontSize: 12
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
marginTop: 6
|
||||
}}
|
||||
onPress={() => {
|
||||
if (fullQualityImages?.isAllowed) {
|
||||
setCompress(!compress);
|
||||
}
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Compress image(s) (recommended)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
<Icon
|
||||
size={20}
|
||||
name={
|
||||
compress
|
||||
? "checkbox-marked"
|
||||
: "checkbox-blank-outline"
|
||||
}
|
||||
allowFontScaling={false}
|
||||
color={
|
||||
compress && fullQualityImages?.isAllowed
|
||||
? colors.primary.accent
|
||||
: colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 3,
|
||||
fontSize: 12
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Compress image(s) (recommended)
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: rawFiles.length > 0 ? 100 : 200,
|
||||
paddingBottom: 15,
|
||||
marginBottom: 10,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
borderBottomWidth: 1
|
||||
}}
|
||||
>
|
||||
<SafeAreaProvider
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
justifyContent: loadingPage ? "center" : undefined,
|
||||
alignItems: loadingPage ? "center" : undefined
|
||||
}}
|
||||
>
|
||||
{!loadingExtension && !loadingPage ? (
|
||||
<>
|
||||
<Editor
|
||||
editorRef={editorRef}
|
||||
onLoad={onLoadEditor}
|
||||
onChange={(html) => {
|
||||
noteContent.current = html;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{loadingPage ? (
|
||||
<>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<Paragraph>Preparing web clip...</Paragraph>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: rawFiles.length > 0 ? 100 : 200,
|
||||
paddingBottom: 15,
|
||||
marginBottom: 10,
|
||||
borderBottomColor: colors.secondary.background,
|
||||
borderBottomWidth: 1
|
||||
}}
|
||||
>
|
||||
<SafeAreaProvider
|
||||
|
||||
{appendNoteId ? (
|
||||
<AppendNote
|
||||
id={appendNoteId}
|
||||
onLoad={(title) => {
|
||||
if (!noteTitle.current) {
|
||||
noteTitle.current = title;
|
||||
inputRef.current?.setNativeProps?.({
|
||||
text: noteTitle.current
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
justifyContent: loadingPage ? "center" : undefined,
|
||||
alignItems: loadingPage ? "center" : undefined
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{!loadingExtension && !loadingPage ? (
|
||||
<>
|
||||
<Editor
|
||||
editorRef={editorRef}
|
||||
onLoad={onLoadEditor}
|
||||
onChange={(html) => {
|
||||
noteContent.current = html;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{loadingPage ? (
|
||||
<>
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
<Paragraph>Preparing web clip...</Paragraph>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</SafeAreaProvider>
|
||||
</View>
|
||||
|
||||
{appendNoteId ? (
|
||||
<AppendNote
|
||||
id={appendNoteId}
|
||||
onLoad={(title) => {
|
||||
if (!noteTitle.current) {
|
||||
noteTitle.current = title;
|
||||
inputRef.current?.setNativeProps?.({
|
||||
text: noteTitle.current
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Clip Mode:
|
||||
</Paragraph>
|
||||
{rawData.value && isURL(rawData.value) ? (
|
||||
<Button
|
||||
type={mode === 2 ? "inverted" : "plain"}
|
||||
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
|
||||
onPress={() => changeMode(2)}
|
||||
title={modes[2].title}
|
||||
height={30}
|
||||
<Paragraph
|
||||
style={{
|
||||
marginRight: 10
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
>
|
||||
Clip Mode:
|
||||
</Paragraph>
|
||||
{rawData.value && isURL(rawData.value) ? (
|
||||
<Button
|
||||
type={mode === 2 ? "inverted" : "plain"}
|
||||
icon={mode === 2 ? "radiobox-marked" : "radiobox-blank"}
|
||||
onPress={() => changeMode(2)}
|
||||
title={modes[2].title}
|
||||
height={30}
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type={mode === 1 ? "inverted" : "plain"}
|
||||
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
|
||||
onPress={() => changeMode(1)}
|
||||
allowFontScaling={false}
|
||||
title={modes[1].title}
|
||||
height={30}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
type={mode === 1 ? "inverted" : "plain"}
|
||||
icon={mode === 1 ? "radiobox-marked" : "radiobox-blank"}
|
||||
onPress={() => changeMode(1)}
|
||||
allowFontScaling={false}
|
||||
title={modes[1].title}
|
||||
height={30}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
flexDirection: "column",
|
||||
marginTop: 10,
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon="plus"
|
||||
onPress={() => {
|
||||
useShareStore.getState().setAppendNote(null);
|
||||
}}
|
||||
type={!appendNoteId ? "transparent" : "plain"}
|
||||
title="New note"
|
||||
allowFontScaling={false}
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 45,
|
||||
width: "100%",
|
||||
marginRight: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.background,
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
icon="text-short"
|
||||
onPress={() => {
|
||||
setSearchMode("appendNote");
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
type={appendNoteId ? "transparent" : "plain"}
|
||||
title={`Append to a note`}
|
||||
style={{
|
||||
borderRadius: 10,
|
||||
flexDirection: "column",
|
||||
marginTop: 10,
|
||||
alignSelf: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: 45,
|
||||
width: "100%",
|
||||
marginRight: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.background,
|
||||
justifyContent: "flex-start"
|
||||
gap: 10
|
||||
}}
|
||||
/>
|
||||
|
||||
{!appendNoteId ? (
|
||||
<AddTags
|
||||
>
|
||||
<Button
|
||||
icon="plus"
|
||||
onPress={() => {
|
||||
setSearchMode("selectTags");
|
||||
useShareStore.getState().setAppendNote(null);
|
||||
}}
|
||||
type={!appendNoteId ? "transparent" : "plain"}
|
||||
title="New note"
|
||||
allowFontScaling={false}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 45,
|
||||
width: "100%",
|
||||
marginRight: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.background,
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!appendNoteId ? (
|
||||
<AddNotebooks
|
||||
<Button
|
||||
icon="text-short"
|
||||
onPress={() => {
|
||||
setSearchMode("selectNotebooks");
|
||||
setSearchMode("appendNote");
|
||||
}}
|
||||
allowFontScaling={false}
|
||||
type={appendNoteId ? "transparent" : "plain"}
|
||||
title={`Append to a note`}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 45,
|
||||
width: "100%",
|
||||
marginRight: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.background,
|
||||
justifyContent: "flex-start"
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
{!appendNoteId ? (
|
||||
<AddTags
|
||||
onPress={() => {
|
||||
setSearchMode("selectTags");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!appendNoteId ? (
|
||||
<AddNotebooks
|
||||
onPress={() => {
|
||||
setSearchMode("selectNotebooks");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
height: isTablet() ? 150 : Platform.OS === "ios" ? 110 : 0
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</WrapperView>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -17,12 +17,13 @@ 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 { db } from "../common/database";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { eSendEvent } from "../services/event-manager";
|
||||
import Navigation from "../services/navigation";
|
||||
import { NotePreviewWidget } from "../services/note-preview-widget";
|
||||
import Notifications from "../services/notifications";
|
||||
import { eAfterSync } from "../utils/events";
|
||||
import { NotesnookModule, ShortcutInfo } from "../utils/notesnook-module";
|
||||
import { useFavoriteStore } from "./use-favorite-store";
|
||||
import { useMenuStore } from "./use-menu-store";
|
||||
import { useMonographStore } from "./use-monograph-store";
|
||||
@@ -34,6 +35,81 @@ import { useTagStore } from "./use-tag-store";
|
||||
import { useTrashStore } from "./use-trash-store";
|
||||
import { useUserStore } from "./use-user-store";
|
||||
|
||||
async function syncShortcuts(result: ShortcutInfo[]) {
|
||||
try {
|
||||
for (let shortcut of result) {
|
||||
switch (shortcut.type) {
|
||||
case "note":
|
||||
{
|
||||
const note = await db.notes.note(shortcut.id);
|
||||
if (!note) {
|
||||
NotesnookModule.removeShortcut(shortcut.id);
|
||||
} else if (note.title !== shortcut.title) {
|
||||
NotesnookModule.updateShortcut(
|
||||
shortcut.id,
|
||||
"note",
|
||||
note.title,
|
||||
note.headline
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "notebook":
|
||||
{
|
||||
const notebook = await db.notebooks.notebook(shortcut.id);
|
||||
if (!notebook) {
|
||||
NotesnookModule.removeShortcut(shortcut.id);
|
||||
} else if (notebook.title !== shortcut.title) {
|
||||
NotesnookModule.updateShortcut(
|
||||
shortcut.id,
|
||||
"notebook",
|
||||
notebook.title,
|
||||
notebook.description
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "tag":
|
||||
{
|
||||
const tag = await db.tags.tag(shortcut.id);
|
||||
if (!tag) {
|
||||
NotesnookModule.removeShortcut(shortcut.id);
|
||||
} else if (tag.title !== shortcut.title) {
|
||||
NotesnookModule.updateShortcut(
|
||||
shortcut.id,
|
||||
"tag",
|
||||
tag.title,
|
||||
tag.title
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "color":
|
||||
{
|
||||
const color = await db.colors.color(shortcut.id);
|
||||
if (!color) {
|
||||
NotesnookModule.removeShortcut(shortcut.id);
|
||||
} else if (color.title !== shortcut.title) {
|
||||
NotesnookModule.updateShortcut(
|
||||
shortcut.id,
|
||||
"color",
|
||||
color.title,
|
||||
color.title,
|
||||
color.colorCode
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(
|
||||
e as Error,
|
||||
"Error while syncing homescreen shortcuts"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function initAfterSync(type: "full" | "send" = "send") {
|
||||
if (type === "full") {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
@@ -46,9 +122,16 @@ export function initAfterSync(type: "full" | "send" = "send") {
|
||||
profile: db.settings.getProfile()
|
||||
});
|
||||
}
|
||||
|
||||
Notifications.setupReminders(true);
|
||||
NotePreviewWidget.updateNotes();
|
||||
eSendEvent(eAfterSync);
|
||||
|
||||
NotesnookModule.getAllShortcuts()
|
||||
.then(syncShortcuts)
|
||||
.catch((e) => {
|
||||
DatabaseLogger.log(e);
|
||||
});
|
||||
}
|
||||
|
||||
export async function initialize() {}
|
||||
|
||||
@@ -25,8 +25,7 @@ import {
|
||||
Note,
|
||||
Notebook,
|
||||
Reminder,
|
||||
Tag,
|
||||
TrashItem
|
||||
Tag
|
||||
} from "@notesnook/core";
|
||||
import { ParamListBase } from "@react-navigation/core";
|
||||
import { create } from "zustand";
|
||||
@@ -36,12 +35,15 @@ export type GenericRouteParam = {
|
||||
};
|
||||
|
||||
export type NotebookScreenParams = {
|
||||
item: Notebook;
|
||||
id: string;
|
||||
item?: Notebook;
|
||||
canGoBack?: boolean;
|
||||
};
|
||||
|
||||
export type NotesScreenParams = {
|
||||
item: Note | Notebook | Tag | Color | TrashItem | Reminder;
|
||||
type: "tag" | "color" | "monograph";
|
||||
id: string;
|
||||
item?: Tag | Color;
|
||||
canGoBack?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ import { initialWindowMetrics } from "react-native-safe-area-context";
|
||||
import { FileType } from "react-native-scoped-storage";
|
||||
import { create } from "zustand";
|
||||
import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
|
||||
import { Reminder } from "@notesnook/core";
|
||||
import { DayFormat, WeekFormat, Reminder } from "@notesnook/core";
|
||||
import { db } from "../common/database";
|
||||
import { EDITOR_LINE_HEIGHT } from "../utils/constants";
|
||||
export const HostIds = [
|
||||
"API_HOST",
|
||||
"AUTH_HOST",
|
||||
@@ -99,6 +101,7 @@ export type Settings = {
|
||||
serverUrls?: Record<HostId, string>;
|
||||
defaultSidebarTab: number;
|
||||
checkForUpdates?: boolean;
|
||||
defaultLineHeight: number;
|
||||
};
|
||||
|
||||
type DimensionsType = {
|
||||
@@ -134,8 +137,12 @@ export interface SettingStore {
|
||||
setInsets: (insets: Insets) => void;
|
||||
timeFormat: string;
|
||||
dateFormat: string;
|
||||
dayFormat: DayFormat;
|
||||
weekFormat: WeekFormat;
|
||||
dbPassword?: string;
|
||||
isOldAppLock: () => boolean;
|
||||
initialUrl: string | null;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const { width, height } = Dimensions.get("window");
|
||||
@@ -198,7 +205,8 @@ export const defaultSettings: SettingStore["settings"] = {
|
||||
backupType: "partial",
|
||||
fullBackupReminder: "never",
|
||||
lastFullBackupDate: 0,
|
||||
checkForUpdates: true
|
||||
checkForUpdates: true,
|
||||
defaultLineHeight: EDITOR_LINE_HEIGHT.DEFAULT
|
||||
};
|
||||
|
||||
export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
@@ -221,6 +229,8 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
setInsets: (insets) => set({ insets }),
|
||||
timeFormat: "12-hour",
|
||||
dateFormat: "DD-MM-YYYY",
|
||||
dayFormat: "short",
|
||||
weekFormat: "Sun",
|
||||
setAppDidEnterBackgroundForAction: (value: boolean) => {
|
||||
set({
|
||||
appDidEnterBackgroundForAction: value
|
||||
@@ -236,5 +246,14 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
},
|
||||
insets: initialWindowMetrics?.insets
|
||||
? initialWindowMetrics.insets
|
||||
: { top: 0, right: 0, left: 0, bottom: 0 }
|
||||
: { top: 0, right: 0, left: 0, bottom: 0 },
|
||||
initialUrl: null,
|
||||
refresh: () => {
|
||||
set({
|
||||
dayFormat: db.settings.getDayFormat(),
|
||||
timeFormat: db.settings.getTimeFormat(),
|
||||
dateFormat: db.settings?.getTimeFormat(),
|
||||
weekFormat: db.settings.getWeekFormat()
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -122,3 +122,9 @@ export const SUBSCRIPTION_PROVIDER = {
|
||||
icon: "web"
|
||||
}
|
||||
};
|
||||
|
||||
export const EDITOR_LINE_HEIGHT = {
|
||||
DEFAULT: 1.2,
|
||||
MAX: 10,
|
||||
MIN: 1
|
||||
};
|
||||
|
||||
@@ -19,6 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { NativeModules, Platform } from "react-native";
|
||||
|
||||
export type ShortcutInfo = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "note" | "notebook" | "tag" | "color";
|
||||
};
|
||||
|
||||
interface NotesnookModuleInterface {
|
||||
getActivityName: () => Promise<string>;
|
||||
setBackgroundColor: (color: string) => void;
|
||||
@@ -41,6 +48,23 @@ interface NotesnookModuleInterface {
|
||||
updateWidgetNote: (noteId: string, data: string) => void;
|
||||
updateReminderWidget: () => void;
|
||||
isGestureNavigationEnabled: () => boolean;
|
||||
addShortcut: (
|
||||
id: string,
|
||||
type: "note" | "notebook" | "tag" | "color",
|
||||
title: string,
|
||||
description?: string,
|
||||
color?: string
|
||||
) => Promise<boolean>;
|
||||
removeShortcut: (id: string) => Promise<boolean>;
|
||||
updateShortcut: (
|
||||
id: string,
|
||||
type: "note" | "notebook" | "tag" | "color",
|
||||
title: string,
|
||||
description?: string,
|
||||
color?: string
|
||||
) => Promise<boolean>;
|
||||
removeAllShortcuts: () => Promise<boolean>;
|
||||
getAllShortcuts: () => Promise<ShortcutInfo[]>;
|
||||
}
|
||||
|
||||
export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
@@ -61,7 +85,12 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
|
||||
hasWidgetNote: () => {},
|
||||
updateWidgetNote: () => {},
|
||||
updateReminderWidget: () => {},
|
||||
isGestureNavigationEnabled: () => true
|
||||
isGestureNavigationEnabled: () => true,
|
||||
addShortcut: () => Promise.resolve(false),
|
||||
removeShortcut: () => Promise.resolve(false),
|
||||
updateShortcut: () => Promise.resolve(false),
|
||||
removeAllShortcuts: () => Promise.resolve(false),
|
||||
getAllShortcuts: () => Promise.resolve([])
|
||||
},
|
||||
android: NativeModules.NNativeModule
|
||||
});
|
||||
|
||||
@@ -134,7 +134,7 @@ describe("NOTE TESTS", () => {
|
||||
.run();
|
||||
});
|
||||
|
||||
it.only("Assign colors to a note", async () => {
|
||||
it("Assign colors to a note", async () => {
|
||||
await TestBuilder.create()
|
||||
.prepare()
|
||||
.createNote()
|
||||
|
||||
@@ -21,7 +21,7 @@ import { notesnook } from "../test.ids";
|
||||
import { TestBuilder } from "./utils";
|
||||
|
||||
describe("Sort & filter", () => {
|
||||
it("Sort by date-edited/date-created", async () => {
|
||||
it.only("Sort by date-edited/date-created", async () => {
|
||||
await TestBuilder.create()
|
||||
.prepare()
|
||||
.createNote("Note 1", "Note 1")
|
||||
@@ -34,7 +34,7 @@ describe("Sort & filter", () => {
|
||||
.element(by.web.className("ProseMirror"))
|
||||
.typeText("Edited ", true);
|
||||
})
|
||||
.pressBack(2)
|
||||
.pressBack(1)
|
||||
.waitAndTapById("icon-sort")
|
||||
.wait(500)
|
||||
.waitAndTapByText("Date created")
|
||||
|
||||
@@ -94,7 +94,7 @@ describe("VAULT", () => {
|
||||
.typeTextById(notesnook.ids.dialogs.vault.pwd, "1234")
|
||||
.typeTextById(notesnook.ids.dialogs.vault.changePwd, "2362")
|
||||
.waitAndTapByText("Change")
|
||||
.pressBack(4)
|
||||
.pressBack(3)
|
||||
.addStep(async () => await openLockedNote("2362"))
|
||||
.run();
|
||||
});
|
||||
|
||||
Binary file not shown.
@@ -1029,7 +1029,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2168;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1104,7 +1104,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.12;
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
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 = 2168;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1210,7 +1210,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.3.12;
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
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 = 2168;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
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.12;
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
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 = 2168;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
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.12;
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
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 = 2168;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
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.12;
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
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 = 2168;
|
||||
CURRENT_PROJECT_VERSION = 2171;
|
||||
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.12;
|
||||
MARKETING_VERSION = 3.3.13;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -45,6 +45,65 @@ PODS:
|
||||
- MMKV (1.3.14):
|
||||
- MMKVCore (~> 1.3.14)
|
||||
- MMKVCore (1.3.14)
|
||||
- NitroCloudUploader (1.0.9):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- NitroModules
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-callinvoker
|
||||
- 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
|
||||
- NitroModules (0.32.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-callinvoker
|
||||
- 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
|
||||
- pop (1.0.12)
|
||||
- RCT-Folly (2024.11.18.00):
|
||||
- boost
|
||||
@@ -2216,7 +2275,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- react-native-upload (6.29.0):
|
||||
- react-native-upload (6.31.0):
|
||||
- React
|
||||
- react-native-view-shot (4.0.3):
|
||||
- boost
|
||||
@@ -3404,6 +3463,8 @@ DEPENDENCIES:
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||
- MMKV
|
||||
- NitroCloudUploader (from `../node_modules/react-native-nitro-cloud-uploader`)
|
||||
- NitroModules (from `../node_modules/react-native-nitro-modules`)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
||||
@@ -3560,6 +3621,10 @@ EXTERNAL SOURCES:
|
||||
hermes-engine:
|
||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||
:tag: hermes-2025-09-01-RNv0.82.0-265ef62ff3eb7289d17e366664ac0da82303e101
|
||||
NitroCloudUploader:
|
||||
:path: "../node_modules/react-native-nitro-cloud-uploader"
|
||||
NitroModules:
|
||||
:path: "../node_modules/react-native-nitro-modules"
|
||||
RCT-Folly:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||
RCTDeprecation:
|
||||
@@ -3819,6 +3884,8 @@ SPEC CHECKSUMS:
|
||||
JWTDecode: 2eed97c2fa46ccaf3049a787004eedf0be474a87
|
||||
MMKV: 7b5df6a8bf785c6705cc490c541b9d8a957c4a64
|
||||
MMKVCore: 3f40b896e9ab522452df9df3ce983471aa2449ba
|
||||
NitroCloudUploader: 62489381ed6b472d87c8c6b62629136ebd2342d1
|
||||
NitroModules: f964d2f3f5b392d0c0303737085cc30e375dc43f
|
||||
pop: d582054913807fd11fd50bfe6a539d91c7e1a55a
|
||||
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
|
||||
RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a
|
||||
@@ -3879,7 +3946,7 @@ SPEC CHECKSUMS:
|
||||
react-native-share-extension: fdc6aaab51591a2d445df239c446aaa3a99658ec
|
||||
react-native-sodium: 066f76e46c9be13e9260521e3fa994937c4cdab4
|
||||
react-native-theme-switch-animation: 449d6db7a760f55740505e7403ae8061debc9a7e
|
||||
react-native-upload: 1459a4be625dda39e4364d9e7d859477a3000f2a
|
||||
react-native-upload: 6285965d20879bcfcf7953f05eb785ba03a60e40
|
||||
react-native-view-shot: 6c008e58f4720de58370848201c5d4a082c6d4ca
|
||||
react-native-webview: 654f794a7686b47491cf43aa67f7f428bea00eed
|
||||
React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
|
||||
|
||||
51
apps/mobile/package-lock.json
generated
51
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13-beta.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -16,7 +16,7 @@
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.5",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.8",
|
||||
"@ammarahmed/react-native-upload": "^6.29.0",
|
||||
"@ammarahmed/react-native-upload": "^6.31.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@callstack/repack": "~5.2.1",
|
||||
@@ -100,6 +100,8 @@
|
||||
"react-native-mmkv-storage": "^12.0.1",
|
||||
"react-native-modal-datetime-picker": "14.0.0",
|
||||
"react-native-navigation-bar-color": "2.0.2",
|
||||
"react-native-nitro-cloud-uploader": "^1.0.9",
|
||||
"react-native-nitro-modules": "^0.32.0",
|
||||
"react-native-notification-sounds": "0.5.5",
|
||||
"react-native-orientation-locker": "^1.7.0",
|
||||
"react-native-pdf": "^7.0.3",
|
||||
@@ -329,9 +331,11 @@
|
||||
"colord": "^2.9.3",
|
||||
"detect-indent": "^7.0.1",
|
||||
"entities": "5.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"katex": "0.16.11",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"nanoid": "5.0.7",
|
||||
"papaparse": "^5.5.3",
|
||||
"prism-themes": "^1.9.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"prosemirror-view": "1.34.2",
|
||||
@@ -339,7 +343,6 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"redent": "^4.0.0",
|
||||
"refractor": "^4.8.1",
|
||||
"simplebar-react": "^3.3.2",
|
||||
"strip-indent": "^4.0.0",
|
||||
"unfurl.js": "^6.4.0"
|
||||
},
|
||||
@@ -348,7 +351,9 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@theme-ui/components": "0.16.1",
|
||||
"@theme-ui/core": "0.16.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/prismjs": "1.26.4",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-color": "^3.0.12",
|
||||
@@ -465,20 +470,22 @@
|
||||
},
|
||||
"../../servers/themes": {
|
||||
"name": "@notesnook/themes-server",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.4",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@notesnook/theme": "file:../../packages/theme",
|
||||
"@orama/orama": "^1.0.8",
|
||||
"@trpc/server": "10.45.2",
|
||||
"async-mutex": "0.5.0",
|
||||
"cloudflare": "^5.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"react": "18.3.1",
|
||||
"util": "^0.12.5",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@notesnook/theme": "file:../../packages/theme",
|
||||
"@types/cors": "^2.8.13",
|
||||
"react": "18.3.1",
|
||||
"esbuild": "0.21.5",
|
||||
"vite-node": "2.1.8"
|
||||
}
|
||||
},
|
||||
@@ -531,9 +538,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@ammarahmed/react-native-upload": {
|
||||
"version": "6.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.29.0.tgz",
|
||||
"integrity": "sha512-k2FJAVR4Nohx4VhfgHuXzLXQpgEnco/PAv9R080X7oY3O12DTgY0ruDvOFwvQGFW0PDIHlXfLfUiMq2kZc/k1A==",
|
||||
"version": "6.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.31.0.tgz",
|
||||
"integrity": "sha512-GwMDd2IND3nuS4l13pQPP8zA/GJoxQKz0pxCGUoWj65fbPuZPnuUA2Rpf4jZzLlgY62vdjI/TqmX+rPiwwCceQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
@@ -17714,6 +17721,30 @@
|
||||
"integrity": "sha512-ZmpLWRocyme1au11e5ZuecMS/UCi57nlzgnioi03Q6ERMbeUOqqbWgNBaNB7SsCeqBV6fZPjo3+A64zEIpzw4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-nitro-cloud-uploader": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/react-native-nitro-cloud-uploader/-/react-native-nitro-cloud-uploader-1.0.9.tgz",
|
||||
"integrity": "sha512-J96JC/yp0DWeyS512zQFYbw2KGiengWs0IAGGoV6NCAystluBJlVO4WqgiCiy/nd3bFidy/+uteC+P5h28j0vw==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-nitro-modules": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-nitro-modules": {
|
||||
"version": "0.32.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.32.0.tgz",
|
||||
"integrity": "sha512-nLDsmi/H9/cLcYxArYxcTDL641JiWnAH3u2LmsFETgyqSEHvukswDTdmX6AHBRIAwXu/iUDppZyjsaRpTG6WMg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-notification-sounds": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-notification-sounds/-/react-native-notification-sounds-0.5.5.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.3.12",
|
||||
"version": "3.3.13",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
@@ -32,7 +32,7 @@
|
||||
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
|
||||
"@ammarahmed/react-native-share-extension": "^2.9.5",
|
||||
"@ammarahmed/react-native-sodium": "^1.6.8",
|
||||
"@ammarahmed/react-native-upload": "^6.29.0",
|
||||
"@ammarahmed/react-native-upload": "^6.31.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@bam.tech/react-native-image-resizer": "3.0.11",
|
||||
"@callstack/repack": "~5.2.1",
|
||||
@@ -116,6 +116,8 @@
|
||||
"react-native-mmkv-storage": "^12.0.1",
|
||||
"react-native-modal-datetime-picker": "14.0.0",
|
||||
"react-native-navigation-bar-color": "2.0.2",
|
||||
"react-native-nitro-cloud-uploader": "^1.0.9",
|
||||
"react-native-nitro-modules": "^0.32.0",
|
||||
"react-native-notification-sounds": "0.5.5",
|
||||
"react-native-orientation-locker": "^1.7.0",
|
||||
"react-native-pdf": "^7.0.3",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
diff --git a/node_modules/react-native-nitro-cloud-uploader/android/src/main/AndroidManifest.xml b/node_modules/react-native-nitro-cloud-uploader/android/src/main/AndroidManifest.xml
|
||||
index 9ea903a..e023649 100644
|
||||
--- a/node_modules/react-native-nitro-cloud-uploader/android/src/main/AndroidManifest.xml
|
||||
+++ b/node_modules/react-native-nitro-cloud-uploader/android/src/main/AndroidManifest.xml
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- Required for background uploads -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
|
||||
<!-- Keep CPU awake during uploads -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
diff --git a/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt b/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt
|
||||
index f93d4dc..f95a537 100644
|
||||
--- a/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt
|
||||
+++ b/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/NitroCloudUploader.kt
|
||||
@@ -388,7 +388,7 @@ class NitroCloudUploader(
|
||||
)
|
||||
|
||||
if (shouldNotify) {
|
||||
- showNotification(uploadId, 100, "Upload complete!", isComplete = true)
|
||||
+// showNotification(uploadId, 100, "Upload complete!", isComplete = true)
|
||||
}
|
||||
|
||||
// ✅ Stop foreground service on success
|
||||
@@ -414,7 +414,7 @@ class NitroCloudUploader(
|
||||
println("❌ Upload failed: ${e.message}")
|
||||
|
||||
if (shouldNotify) {
|
||||
- showNotification(uploadId, -1, "Upload failed", isComplete = true)
|
||||
+// showNotification(uploadId, -1, "Upload failed", isComplete = true)
|
||||
}
|
||||
|
||||
// ✅ Stop foreground service on failure
|
||||
@@ -458,7 +458,7 @@ class NitroCloudUploader(
|
||||
)
|
||||
|
||||
if (shouldNotify) {
|
||||
- showNotification(uploadId, 0, "Starting upload...")
|
||||
+// showNotification(uploadId, 0, "Starting upload...")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -672,7 +672,7 @@ class NitroCloudUploader(
|
||||
|
||||
if (showNotification) {
|
||||
val progressPercent = (progress * 100).toInt()
|
||||
- showNotification(uploadId, progressPercent, "Uploading... ${progressPercent}%")
|
||||
+// showNotification(uploadId, progressPercent, "Uploading... ${progressPercent}%")
|
||||
|
||||
// ✅ Update foreground service notification
|
||||
val ctx = appContext // Cache to avoid smart cast issues
|
||||
diff --git a/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/UploadForegroundService.kt b/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/UploadForegroundService.kt
|
||||
index 466c212..e261329 100644
|
||||
--- a/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/UploadForegroundService.kt
|
||||
+++ b/node_modules/react-native-nitro-cloud-uploader/android/src/main/java/com/margelo/nitro/nitroclouduploader/UploadForegroundService.kt
|
||||
@@ -134,7 +134,7 @@ class UploadForegroundService : Service() {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
notification,
|
||||
- ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
)
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
@@ -165,6 +165,7 @@ class UploadForegroundService : Service() {
|
||||
|
||||
isServiceStarted = false
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
+ notificationManager.cancel(NOTIFICATION_ID);
|
||||
stopSelf()
|
||||
println("✅ Foreground service stopped")
|
||||
}
|
||||
@@ -201,7 +202,7 @@ class UploadForegroundService : Service() {
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
- .setContentTitle("Cloud Uploader")
|
||||
+ .setContentTitle("Uploading file")
|
||||
.setContentText(message)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_upload)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
@@ -115,7 +115,13 @@ const EXTRA_ICON_NAMES = [
|
||||
"update",
|
||||
"notebook-minus",
|
||||
"calendar-blank",
|
||||
"email-newsletter"
|
||||
"email-newsletter",
|
||||
"cellphone-arrow-down",
|
||||
"format-line-spacing",
|
||||
"calendar-today",
|
||||
"bomb",
|
||||
"bomb-off",
|
||||
"cancel"
|
||||
];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
File diff suppressed because one or more lines are too long
43
apps/monograph/package-lock.json
generated
43
apps/monograph/package-lock.json
generated
@@ -179,9 +179,11 @@
|
||||
"colord": "^2.9.3",
|
||||
"detect-indent": "^7.0.1",
|
||||
"entities": "5.0.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"katex": "0.16.11",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"nanoid": "5.0.7",
|
||||
"papaparse": "^5.5.3",
|
||||
"prism-themes": "^1.9.0",
|
||||
"prosemirror-codemark": "^0.4.2",
|
||||
"prosemirror-view": "1.34.2",
|
||||
@@ -189,7 +191,6 @@
|
||||
"react-colorful": "^5.6.1",
|
||||
"redent": "^4.0.0",
|
||||
"refractor": "^4.8.1",
|
||||
"simplebar-react": "^3.3.2",
|
||||
"strip-indent": "^4.0.0",
|
||||
"unfurl.js": "^6.4.0"
|
||||
},
|
||||
@@ -198,7 +199,9 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@theme-ui/components": "0.16.1",
|
||||
"@theme-ui/core": "0.16.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/katex": "0.16.7",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/prismjs": "1.26.4",
|
||||
"@types/react": "18.3.5",
|
||||
"@types/react-color": "^3.0.12",
|
||||
@@ -2947,22 +2950,6 @@
|
||||
"@styled-system/css": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@theme-ui/color-modes": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@theme-ui/color-modes/-/color-modes-0.16.2.tgz",
|
||||
"integrity": "sha512-jWEWx53lxNgWCT38i/kwLV2rsvJz8lVZgi5oImnVwYba9VejXD23q1ckbNFJHosQ8KKXY87ht0KPC6BQFIiHtQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@theme-ui/core": "^0.16.2",
|
||||
"@theme-ui/css": "^0.16.2",
|
||||
"deepmerge": "^4.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@theme-ui/components": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@theme-ui/components/-/components-0.16.2.tgz",
|
||||
@@ -3008,22 +2995,6 @@
|
||||
"@emotion/react": "^11.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@theme-ui/theme-provider": {
|
||||
"version": "0.16.2",
|
||||
"resolved": "https://registry.npmjs.org/@theme-ui/theme-provider/-/theme-provider-0.16.2.tgz",
|
||||
"integrity": "sha512-LRnVevODcGqO0JyLJ3wht+PV3ZoZcJ7XXLJAJWDoGeII4vZcPQKwVy4Lpz/juHsZppQxKcB3U+sQDGBnP25irQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@theme-ui/color-modes": "^0.16.2",
|
||||
"@theme-ui/core": "^0.16.2",
|
||||
"@theme-ui/css": "^0.16.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/acorn": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz",
|
||||
@@ -3153,14 +3124,14 @@
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
|
||||
"integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -12268,7 +12239,7 @@
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
4
apps/theme-builder/package-lock.json
generated
4
apps/theme-builder/package-lock.json
generated
@@ -987,7 +987,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.3.4",
|
||||
"version": "3.3.8-beta.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -1069,7 +1069,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.5",
|
||||
"@cloudflare/workers-types": "^4.20250224.0",
|
||||
"@playwright/test": "1.48.2",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@swc/plugin-react-remove-properties": "^6.0.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
|
||||
@@ -415,6 +415,44 @@ test("when autosave is disabled, closing the note should save it", async ({
|
||||
expect(await notes.editor.getContent("text")).toBe(content.trim());
|
||||
});
|
||||
|
||||
test("when autosave is disabled, clicking the not saved icon should save the note", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
await notes.createNote({
|
||||
title: NOTE.title,
|
||||
content
|
||||
});
|
||||
|
||||
await notes.editor.notSavedIcon.waitFor({ state: "visible" });
|
||||
await notes.editor.notSavedIcon.click();
|
||||
|
||||
await expect(notes.editor.savedIcon).toBeVisible();
|
||||
expect(await notes.editor.getContent("text")).toBe(content.trim());
|
||||
});
|
||||
|
||||
test("when autosave is disabled, clicking save from tab menu should save the note", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const content = "a ".repeat(100);
|
||||
await notes.createNote({
|
||||
title: NOTE.title,
|
||||
content
|
||||
});
|
||||
|
||||
const tab = (await notes.editor.getTabs())[0];
|
||||
await tab.contextMenu.save();
|
||||
|
||||
await expect(notes.editor.savedIcon).toBeVisible();
|
||||
expect(await notes.editor.getContent("text")).toBe(content.trim());
|
||||
});
|
||||
|
||||
test("control + alt + right arrow should go to next note", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
iterateList
|
||||
} from "./utils";
|
||||
import { SessionHistoryItemModel } from "./session-history-item-model";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
abstract class BaseProperties {
|
||||
protected readonly page: Page;
|
||||
@@ -222,6 +223,42 @@ export class NotePropertiesModel extends BaseProperties {
|
||||
await this.close();
|
||||
return history;
|
||||
}
|
||||
|
||||
async getDateCreated() {
|
||||
await this.open();
|
||||
const dateCreated = await this.generalSection
|
||||
.locator(getTestId("date-created"))
|
||||
.textContent();
|
||||
await this.close();
|
||||
return dateCreated;
|
||||
}
|
||||
|
||||
async editDateCreated(newDateCreated: number) {
|
||||
await this.open();
|
||||
|
||||
const editIcon = this.page.locator(getTestId("edit-date-created"));
|
||||
await editIcon.click();
|
||||
|
||||
const editDateCreatedDialog = this.page.locator(
|
||||
getTestId("edit-note-creation-date-dialog")
|
||||
);
|
||||
await editDateCreatedDialog.waitFor({ state: "visible" });
|
||||
|
||||
const dateInput = editDateCreatedDialog.locator(
|
||||
getTestId("date-created-input")
|
||||
);
|
||||
const timeInput = editDateCreatedDialog.locator(
|
||||
getTestId("time-created-input")
|
||||
);
|
||||
|
||||
const date = new Date(newDateCreated);
|
||||
await dateInput.fill(dayjs(date).format("DD-MM-YYYY"));
|
||||
await timeInput.fill(dayjs(date).format("hh:mm A"));
|
||||
|
||||
await confirmDialog(editDateCreatedDialog);
|
||||
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
|
||||
export class NoteContextMenuModel extends BaseProperties {
|
||||
|
||||
@@ -74,4 +74,9 @@ class TabContextMenuModel {
|
||||
await this.open();
|
||||
return this.menu.getItem("reveal-in-list");
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.open();
|
||||
return this.menu.clickOnItem("save");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import {
|
||||
getTestId,
|
||||
groupByOptions,
|
||||
NOTE,
|
||||
orderByOptions,
|
||||
@@ -447,3 +446,16 @@ test("archived note shouldn't appear in search results", async ({ page }) => {
|
||||
const searchedNote = await notes.findNote(NOTE);
|
||||
expect(searchedNote).toBeUndefined();
|
||||
});
|
||||
|
||||
test("edit note creation date in properties panel", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
|
||||
const date = new Date("2020-01-15T10:30:00Z");
|
||||
await note?.properties.editDateCreated(date.valueOf());
|
||||
|
||||
const dateCreated = await note?.properties.getDateCreated();
|
||||
expect(dateCreated).toBe("15-01-2020 03:30 PM");
|
||||
});
|
||||
|
||||
1427
apps/web/package-lock.json
generated
1427
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.3.7",
|
||||
"version": "3.3.8",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
|
||||
@@ -24,7 +24,11 @@ import { useEditorStore } from "./stores/editor-store";
|
||||
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { useStore as useSettingStore } from "./stores/setting-store";
|
||||
import { scheduleBackups, scheduleFullBackups } from "./common/notices";
|
||||
import { introduceFeatures, resetFeatures } from "./common";
|
||||
import {
|
||||
introduceFeatures,
|
||||
resetFeatures,
|
||||
scheduleExpiredNotesDeletion
|
||||
} from "./common";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { db } from "./common/db";
|
||||
import { EV, EVENTS } from "@notesnook/core";
|
||||
@@ -65,6 +69,7 @@ export default function AppEffects() {
|
||||
await FeatureDialog.show({ featureName: "highlights" });
|
||||
await scheduleBackups();
|
||||
await scheduleFullBackups();
|
||||
await scheduleExpiredNotesDeletion();
|
||||
if (useSettingStore.getState().isFullOfflineMode)
|
||||
// NOTE: we deliberately don't await here because we don't want to pause execution.
|
||||
db.attachments.cacheAttachments().catch(logger.error);
|
||||
|
||||
@@ -115,13 +115,13 @@ textarea,
|
||||
}
|
||||
|
||||
.rpv-core__text-layer-text::selection {
|
||||
background-color: var(--background-selected) !important;
|
||||
background-color: var(--accent-selected) !important;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.rpv-core__text-layer-text::-moz-selection {
|
||||
/* Code for Firefox */
|
||||
background-color: var(--background-selected) !important;
|
||||
background-color: var(--accent-selected) !important;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ 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();
|
||||
|
||||
@@ -144,10 +143,6 @@ 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);
|
||||
|
||||
@@ -22,6 +22,10 @@ import { logger } from "../utils/logger";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { db } from "./db";
|
||||
import { checkUpload, decryptFile, saveFile } from "../interfaces/fs";
|
||||
import { ScopedThemeProvider } from "../components/theme-provider";
|
||||
import { Lightbox } from "../components/lightbox";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Attachment } from "@notesnook/core";
|
||||
|
||||
async function download(hash: string, groupId?: string) {
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
@@ -127,3 +131,29 @@ export async function checkAttachment(hash: string) {
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export async function previewImageAttachment(attachment: Attachment) {
|
||||
const container = document.getElementById("dialogContainer");
|
||||
if (!(container instanceof HTMLElement)) return;
|
||||
|
||||
const dataurl = await downloadAttachment(
|
||||
attachment.hash,
|
||||
"base64",
|
||||
attachment.id
|
||||
);
|
||||
if (!dataurl) {
|
||||
return showToast("error", strings.imagePreviewFailed());
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<ScopedThemeProvider>
|
||||
<Lightbox
|
||||
image={dataurl}
|
||||
onClose={() => {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}}
|
||||
/>
|
||||
</ScopedThemeProvider>,
|
||||
container
|
||||
);
|
||||
}
|
||||
@@ -62,6 +62,7 @@ import { showFeatureNotAllowedToast } from "./toasts";
|
||||
import { UpgradeDialog } from "../dialogs/buy-dialog/upgrade-dialog";
|
||||
import { setToolbarPreset } from "./toolbar-config";
|
||||
import { useKeyStore } from "../interfaces/key-store";
|
||||
import { TaskScheduler } from "../utils/task-scheduler";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
@@ -560,3 +561,11 @@ export async function resetFeatures() {
|
||||
db.settings.setDefaultTag(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export async function scheduleExpiredNotesDeletion() {
|
||||
await db.notes.deleteExpiredNotes();
|
||||
await TaskScheduler.stop("delete-expired-notes");
|
||||
TaskScheduler.register("delete-expired-notes", "0 0 * * *", async () => {
|
||||
await db.notes.deleteExpiredNotes();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import dayjs from "dayjs";
|
||||
import { hardNavigate, hashNavigate } from "../navigation";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { TaskScheduler } from "../utils/task-scheduler";
|
||||
import { BuyDialog } from "../dialogs/buy-dialog";
|
||||
import { RecoveryKeyDialog } from "../dialogs/recovery-key-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { SettingsDialog } from "../dialogs/settings";
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
FileWebClip,
|
||||
Icon,
|
||||
Loading,
|
||||
PasswordInvisible,
|
||||
References,
|
||||
Rename,
|
||||
Reupload,
|
||||
@@ -40,7 +41,11 @@ import {
|
||||
} from "../icons";
|
||||
import { store, useStore } from "../../stores/attachment-store";
|
||||
import { db } from "../../common/db";
|
||||
import { saveAttachment } from "../../common/attachments";
|
||||
import {
|
||||
downloadAttachment,
|
||||
previewImageAttachment,
|
||||
saveAttachment
|
||||
} from "../../common/attachments";
|
||||
import { reuploadAttachment } from "../editor/picker";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { Menu } from "../../hooks/use-menu";
|
||||
@@ -59,6 +64,8 @@ import { PromptDialog } from "../../dialogs/prompt";
|
||||
import { DialogManager } from "../../common/dialog-manager";
|
||||
import { useStore as useSelectionStore } from "../../stores/selection-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { PdfPreviewDialog } from "../../dialogs/pdf-preview-dialog";
|
||||
|
||||
const FILE_ICONS: Record<string, Icon> = {
|
||||
"image/": FileImage,
|
||||
@@ -249,6 +256,34 @@ const AttachmentMenuItems: (
|
||||
status?: AttachmentProgressStatus
|
||||
) => MenuItem[] = (attachment, status) => {
|
||||
return [
|
||||
{
|
||||
key: "preview-attachment",
|
||||
type: "button",
|
||||
title: strings.previewAttachment(),
|
||||
icon: PasswordInvisible.path,
|
||||
isHidden:
|
||||
!attachment.mimeType.startsWith("image/") &&
|
||||
attachment.mimeType !== PDFMimeType,
|
||||
onClick: async () => {
|
||||
if (attachment.mimeType.startsWith("image")) {
|
||||
await previewImageAttachment(attachment);
|
||||
} else if (attachment.mimeType === PDFMimeType) {
|
||||
const blob = await downloadAttachment(
|
||||
attachment.hash,
|
||||
"blob",
|
||||
attachment.id
|
||||
);
|
||||
if (!blob) {
|
||||
return showToast("error", strings.attachmentPreviewFailed());
|
||||
}
|
||||
|
||||
PdfPreviewDialog.show({
|
||||
url: URL.createObjectURL(blob),
|
||||
hash: attachment.hash
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "notes",
|
||||
type: "button",
|
||||
|
||||
@@ -44,6 +44,7 @@ type DialogProps = SxProp & {
|
||||
) => void;
|
||||
onOpen?: () => void;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
showCloseButton?: boolean;
|
||||
textAlignment?: "left" | "right" | "center";
|
||||
buttonsAlignment?: "start" | "center" | "end";
|
||||
@@ -98,7 +99,7 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "90%", props.width || "380px"],
|
||||
maxHeight: ["100%", "80%", "70%"],
|
||||
maxHeight: ["100%", "80%", props.height || "70%"],
|
||||
height: ["100%", "auto", "auto"],
|
||||
bg: "background",
|
||||
alignSelf: "center",
|
||||
|
||||
@@ -17,13 +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 {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useState } from "react";
|
||||
import { Flex, Text, Button } from "@theme-ui/components";
|
||||
import { Copy, Restore } from "../icons";
|
||||
import ContentToggle from "./content-toggle";
|
||||
@@ -59,6 +53,8 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
|
||||
const onResolveContent = useCallback(
|
||||
(saveCopy: boolean) => {
|
||||
if (!conflictedContent) return;
|
||||
|
||||
const toKeep =
|
||||
selectedContent === 1 ? session.content?.conflicted : session.content;
|
||||
const toCopy =
|
||||
@@ -69,7 +65,7 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
toKeep,
|
||||
toCopy: saveCopy ? toCopy : undefined,
|
||||
toKeepDateEdited: toKeep.dateEdited,
|
||||
dateResolved: conflictedContent!.dateModified
|
||||
dateResolved: conflictedContent.dateModified
|
||||
});
|
||||
},
|
||||
[conflictedContent, selectedContent, session.content, session.note]
|
||||
@@ -82,8 +78,6 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
});
|
||||
}, [session]);
|
||||
|
||||
if (!conflictedContent || !content) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={(el) => {
|
||||
@@ -105,11 +99,16 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
textAlign: "center"
|
||||
textAlign: "center",
|
||||
paddingTop: 10
|
||||
}}
|
||||
>
|
||||
{session.note.title}
|
||||
</Text>
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
session.type === "diff" && session.oldTitle
|
||||
? diff(session.oldTitle || "", session.note.title)
|
||||
: session.note.title
|
||||
}}
|
||||
></Text>
|
||||
<Flex mt={1} sx={{ alignSelf: "center", justifySelf: "center" }}>
|
||||
{session.type === "diff" ? (
|
||||
<>
|
||||
@@ -140,28 +139,34 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
<Restore size={18} />
|
||||
<Text ml={1}>{strings.restoreThisVersion()}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const { closeTabs, openSession } = useEditorStore.getState();
|
||||
{!content || !conflictedContent ? null : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const { closeTabs, openSession } = useEditorStore.getState();
|
||||
|
||||
const noteId = await createCopy(session.note, content);
|
||||
const noteId = await createCopy(
|
||||
session.note,
|
||||
content,
|
||||
session.oldTitle
|
||||
);
|
||||
|
||||
closeTabs(session.id);
|
||||
closeTabs(session.id);
|
||||
|
||||
await notesStore.refresh();
|
||||
await openSession(noteId);
|
||||
}}
|
||||
mr={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<Copy size={18} />
|
||||
<Text ml={1}>{strings.saveACopy()}</Text>
|
||||
</Button>
|
||||
await notesStore.refresh();
|
||||
await openSession(noteId);
|
||||
}}
|
||||
mr={2}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<Copy size={18} />
|
||||
<Text ml={1}>{strings.saveACopy()}</Text>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="errorSecondary"
|
||||
onClick={() => {
|
||||
@@ -174,165 +179,183 @@ function DiffViewer(props: DiffViewerProps) {
|
||||
</>
|
||||
) : null}
|
||||
</Flex>
|
||||
<ScrollSync>
|
||||
<Flex
|
||||
{!conflictedContent || !content ? (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: ["column", "column", "row"],
|
||||
overflow: "hidden"
|
||||
px: 2,
|
||||
py: 1,
|
||||
my: 1,
|
||||
borderTop: "1px solid var(--border)"
|
||||
}}
|
||||
>
|
||||
{strings.noContent()}
|
||||
</Text>
|
||||
) : (
|
||||
<ScrollSync>
|
||||
<Flex
|
||||
className="firstEditor"
|
||||
data-test-id="first-editor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"]
|
||||
flexDirection: ["column", "column", "row"],
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{content.locked ? (
|
||||
<UnlockView
|
||||
title={getFormattedDate(content.dateEdited)}
|
||||
subtitle={strings.enterPasswordToUnlockVersion()}
|
||||
buttonTitle={strings.unlock()}
|
||||
unlock={async (password) => {
|
||||
const decryptedContent = await db.vault.decryptContent(
|
||||
content,
|
||||
password
|
||||
);
|
||||
setContent({
|
||||
...content,
|
||||
...decryptedContent,
|
||||
locked: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContentToggle
|
||||
label={
|
||||
session.type === "diff"
|
||||
? strings.olderVersion()
|
||||
: strings.currentNote()
|
||||
}
|
||||
readonly={session.type === "diff"}
|
||||
dateEdited={content.dateEdited}
|
||||
isSelected={selectedContent === 0}
|
||||
isOtherSelected={selectedContent === 1}
|
||||
onToggle={() => setSelectedContent((s) => (s === 0 ? -1 : 0))}
|
||||
resolveConflict={onResolveContent}
|
||||
sx={{
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1
|
||||
<Flex
|
||||
className="firstEditor"
|
||||
data-test-id="first-editor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"]
|
||||
}}
|
||||
>
|
||||
{content.locked ? (
|
||||
<UnlockView
|
||||
title={getFormattedDate(content.dateEdited)}
|
||||
subtitle={strings.enterPasswordToUnlockVersion()}
|
||||
buttonTitle={strings.unlock()}
|
||||
unlock={async (password) => {
|
||||
const decryptedContent = await db.vault.decryptContent(
|
||||
content,
|
||||
password
|
||||
);
|
||||
setContent({
|
||||
...content,
|
||||
...decryptedContent,
|
||||
locked: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollSyncPane>
|
||||
<Flex
|
||||
) : (
|
||||
<>
|
||||
<ContentToggle
|
||||
label={
|
||||
session.type === "diff"
|
||||
? strings.olderVersion()
|
||||
: strings.currentNote()
|
||||
}
|
||||
readonly={session.type === "diff"}
|
||||
dateEdited={content.dateEdited}
|
||||
isSelected={selectedContent === 0}
|
||||
isOtherSelected={selectedContent === 1}
|
||||
onToggle={() =>
|
||||
setSelectedContent((s) => (s === 0 ? -1 : 0))
|
||||
}
|
||||
resolveConflict={onResolveContent}
|
||||
sx={{
|
||||
px: 2,
|
||||
overflowY: "auto",
|
||||
flex: 1,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderRightWidth: [0, 0, 1],
|
||||
borderBottomWidth: [1, 1, 0],
|
||||
borderColor: "border"
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
id={content.id}
|
||||
content={() => content.data}
|
||||
session={session}
|
||||
nonce={content.dateEdited}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex
|
||||
className="secondEditor"
|
||||
data-test-id="second-editor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"],
|
||||
borderLeft: conflictedContent.locked
|
||||
? "1px solid var(--border)"
|
||||
: "none"
|
||||
}}
|
||||
>
|
||||
{conflictedContent.locked ? (
|
||||
<UnlockView
|
||||
title={getFormattedDate(conflictedContent.dateEdited)}
|
||||
subtitle={strings.enterPasswordToUnlockVersion()}
|
||||
buttonTitle={strings.unlock()}
|
||||
unlock={async (password) => {
|
||||
const decryptedContent = await db.vault.decryptContent(
|
||||
conflictedContent,
|
||||
password
|
||||
);
|
||||
setConflictedContent({
|
||||
...conflictedContent,
|
||||
...decryptedContent,
|
||||
locked: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ContentToggle
|
||||
readonly={session.type === "diff"}
|
||||
resolveConflict={onResolveContent}
|
||||
label={
|
||||
session.type === "diff"
|
||||
? strings.currentNote()
|
||||
: strings.incomingNote()
|
||||
}
|
||||
isSelected={selectedContent === 1}
|
||||
isOtherSelected={selectedContent === 0}
|
||||
dateEdited={conflictedContent.dateEdited}
|
||||
onToggle={() => setSelectedContent((s) => (s === 1 ? -1 : 1))}
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1,
|
||||
pt: [1, 1, 0]
|
||||
/>
|
||||
|
||||
<ScrollSyncPane>
|
||||
<Flex
|
||||
sx={{
|
||||
px: 2,
|
||||
overflowY: "auto",
|
||||
flex: 1,
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderRightWidth: [0, 0, 1],
|
||||
borderBottomWidth: [1, 1, 0],
|
||||
borderColor: "border"
|
||||
}}
|
||||
>
|
||||
<Editor
|
||||
id={content.id}
|
||||
content={() => content.data}
|
||||
session={session}
|
||||
nonce={content.dateEdited}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex
|
||||
className="secondEditor"
|
||||
data-test-id="second-editor"
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
flexDirection: "column",
|
||||
width: ["100%", "100%", "50%"],
|
||||
height: ["50%", "50%", "100%"],
|
||||
borderLeft: conflictedContent?.locked
|
||||
? "1px solid var(--border)"
|
||||
: "none"
|
||||
}}
|
||||
>
|
||||
{conflictedContent.locked ? (
|
||||
<UnlockView
|
||||
title={getFormattedDate(conflictedContent.dateEdited)}
|
||||
subtitle={strings.enterPasswordToUnlockVersion()}
|
||||
buttonTitle={strings.unlock()}
|
||||
unlock={async (password) => {
|
||||
const decryptedContent = await db.vault.decryptContent(
|
||||
conflictedContent,
|
||||
password
|
||||
);
|
||||
setConflictedContent({
|
||||
...conflictedContent,
|
||||
...decryptedContent,
|
||||
locked: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Flex sx={{ px: 2, overflow: "auto" }}>
|
||||
<Editor
|
||||
id={`${conflictedContent.id}-conflicted`}
|
||||
session={session}
|
||||
content={() =>
|
||||
content.locked
|
||||
? conflictedContent.data
|
||||
: diff(content.data, conflictedContent.data)
|
||||
}
|
||||
nonce={conflictedContent.dateEdited}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</>
|
||||
)}
|
||||
) : (
|
||||
<>
|
||||
<ContentToggle
|
||||
readonly={session.type === "diff"}
|
||||
resolveConflict={onResolveContent}
|
||||
label={
|
||||
session.type === "diff"
|
||||
? strings.currentNote()
|
||||
: strings.incomingNote()
|
||||
}
|
||||
isSelected={selectedContent === 1}
|
||||
isOtherSelected={selectedContent === 0}
|
||||
dateEdited={conflictedContent.dateEdited}
|
||||
onToggle={() =>
|
||||
setSelectedContent((s) => (s === 1 ? -1 : 1))
|
||||
}
|
||||
sx={{
|
||||
alignItems: "flex-end",
|
||||
borderStyle: "solid",
|
||||
borderWidth: 0,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: "border",
|
||||
px: 2,
|
||||
pb: 1,
|
||||
pt: [1, 1, 0]
|
||||
}}
|
||||
/>
|
||||
<ScrollSyncPane>
|
||||
<Flex sx={{ px: 2, overflow: "auto" }}>
|
||||
<Editor
|
||||
id={`${conflictedContent.id}-conflicted`}
|
||||
session={session}
|
||||
content={() =>
|
||||
content.locked
|
||||
? conflictedContent.data
|
||||
: diff(content.data, conflictedContent.data)
|
||||
}
|
||||
nonce={conflictedContent.dateEdited}
|
||||
options={{ readonly: true, headless: true }}
|
||||
/>
|
||||
</Flex>
|
||||
</ScrollSyncPane>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ScrollSync>
|
||||
</ScrollSync>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -374,7 +397,7 @@ async function resolveConflict({
|
||||
await notesStore.refresh();
|
||||
}
|
||||
|
||||
async function createCopy(note: Note, content: ContentItem) {
|
||||
async function createCopy(note: Note, content: ContentItem, title?: string) {
|
||||
if (content.locked) {
|
||||
const contentId = await db.content.add({
|
||||
locked: true,
|
||||
@@ -384,7 +407,7 @@ async function createCopy(note: Note, content: ContentItem) {
|
||||
});
|
||||
return await db.notes.add({
|
||||
contentId,
|
||||
title: note.title + " (COPY)"
|
||||
title: title || note.title + " (COPY)"
|
||||
});
|
||||
} else {
|
||||
return await db.notes.add({
|
||||
@@ -392,7 +415,7 @@ async function createCopy(note: Note, content: ContentItem) {
|
||||
type: "tiptap",
|
||||
data: content.data
|
||||
},
|
||||
title: note.title + " (COPY)"
|
||||
title: title || note.title + " (COPY)"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ import useTablet from "../../hooks/use-tablet";
|
||||
import { isMac } from "../../utils/platform";
|
||||
import { CREATE_BUTTON_MAP } from "../../common";
|
||||
import { getDragData } from "../../utils/data-transfer";
|
||||
import { saveContent } from "./index";
|
||||
|
||||
type ToolButton = {
|
||||
title: string;
|
||||
@@ -162,7 +163,6 @@ export function EditorActionBar() {
|
||||
icon: TableOfContents,
|
||||
enabled:
|
||||
activeSession &&
|
||||
activeSession.type !== "new" &&
|
||||
activeSession.type !== "locked" &&
|
||||
activeSession.type !== "diff" &&
|
||||
activeSession.type !== "conflicted",
|
||||
@@ -406,6 +406,13 @@ const TabStrip = React.memo(function TabStrip() {
|
||||
isLocked={isLockedSession(session)}
|
||||
isRevealInListDisabled={isFocusMode}
|
||||
type={session.type}
|
||||
onSave={() => {
|
||||
const { activeEditorId, getEditor } =
|
||||
useEditorManager.getState();
|
||||
const editor = getEditor(activeEditorId || "")?.editor;
|
||||
if (!editor) return;
|
||||
saveContent(session.id, false, editor.getContent());
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (tab.id !== currentTab) {
|
||||
useEditorStore.getState().activateSession(tab.sessionId);
|
||||
@@ -486,6 +493,7 @@ type TabProps = {
|
||||
onCloseToTheLeft: () => void;
|
||||
onCloseAll: () => void;
|
||||
onPin: () => void;
|
||||
onSave: () => void;
|
||||
onRevealInList?: () => void;
|
||||
};
|
||||
function Tab(props: TabProps) {
|
||||
@@ -505,7 +513,8 @@ function Tab(props: TabProps) {
|
||||
onCloseToTheRight,
|
||||
onCloseToTheLeft,
|
||||
onRevealInList,
|
||||
onPin
|
||||
onPin,
|
||||
onSave
|
||||
} = props;
|
||||
const Icon = isLocked
|
||||
? type === "locked"
|
||||
@@ -580,6 +589,14 @@ function Tab(props: TabProps) {
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
Menu.openMenu([
|
||||
{
|
||||
type: "button",
|
||||
title: strings.save(),
|
||||
key: "save",
|
||||
onClick: onSave,
|
||||
isHidden: !isUnsaved
|
||||
},
|
||||
{ type: "separator", key: "sep0", isHidden: !isUnsaved },
|
||||
{
|
||||
type: "button",
|
||||
title: strings.close(),
|
||||
@@ -610,7 +627,7 @@ function Tab(props: TabProps) {
|
||||
key: "close-all",
|
||||
onClick: onCloseAll
|
||||
},
|
||||
{ type: "separator", key: "sep" },
|
||||
{ type: "separator", key: "sep1" },
|
||||
{
|
||||
type: "button",
|
||||
title: strings.revealInList(),
|
||||
|
||||
@@ -23,3 +23,9 @@ export const EDITOR_ZOOM = {
|
||||
MIN: 30,
|
||||
STEP: 10
|
||||
};
|
||||
|
||||
export const EDITOR_LINE_HEIGHT = {
|
||||
DEFAULT: 1.2,
|
||||
MAX: 10,
|
||||
MIN: 1
|
||||
};
|
||||
|
||||
@@ -34,7 +34,11 @@ import {
|
||||
NormalMode,
|
||||
Cross
|
||||
} from "../icons";
|
||||
import { useEditorConfig, useNoteStatistics } from "./manager";
|
||||
import {
|
||||
useEditorConfig,
|
||||
useEditorManager,
|
||||
useNoteStatistics
|
||||
} from "./manager";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { MAX_AUTO_SAVEABLE_WORDS, NoteStatistics } from "./types";
|
||||
import { strings } from "@notesnook/intl";
|
||||
@@ -43,6 +47,7 @@ import { useWindowControls } from "../../hooks/use-window-controls";
|
||||
import { exitFullscreen } from "../../utils/fullscreen";
|
||||
import { useRef, useState } from "react";
|
||||
import { PopupPresenter } from "@notesnook/ui";
|
||||
import { saveContent } from "./index";
|
||||
|
||||
const SAVE_STATE_ICON_MAP = {
|
||||
"-1": NotSaved,
|
||||
@@ -276,6 +281,24 @@ function EditorFooter() {
|
||||
? "icon-error"
|
||||
: "paragraph"
|
||||
}
|
||||
title={
|
||||
saveState === SaveState.NotSaved ? strings.clickToSave() : undefined
|
||||
}
|
||||
sx={{
|
||||
height: "100%",
|
||||
cursor: saveState === SaveState.NotSaved ? "pointer" : "default",
|
||||
":hover": {
|
||||
bg: saveState === SaveState.NotSaved ? "hover" : "initial"
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (saveState === SaveState.NotSaved) {
|
||||
const { activeEditorId, getEditor } = useEditorManager.getState();
|
||||
const editor = getEditor(activeEditorId || "")?.editor;
|
||||
if (!editor) return;
|
||||
saveContent(session.id, false, editor.getContent());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -26,7 +26,6 @@ import React, {
|
||||
useLayoutEffect,
|
||||
useCallback
|
||||
} from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Box, Button, Flex, Progress, Text } from "@theme-ui/components";
|
||||
import Properties from "../properties";
|
||||
import {
|
||||
@@ -53,13 +52,16 @@ import Header from "./header";
|
||||
import { Attachment } from "../icons";
|
||||
import { attachFiles, AttachmentProgress, insertAttachments } from "./picker";
|
||||
import { useEditorManager } from "./manager";
|
||||
import { saveAttachment, downloadAttachment } from "../../common/attachments";
|
||||
import {
|
||||
saveAttachment,
|
||||
downloadAttachment,
|
||||
previewImageAttachment
|
||||
} from "../../common/attachments";
|
||||
import { EV, EVENTS } from "@notesnook/core";
|
||||
import { db } from "../../common/db";
|
||||
import Titlebox from "./title-box";
|
||||
import Titlebox, { resizeTextarea } from "./title-box";
|
||||
import Config from "../../utils/config";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { Lightbox } from "../lightbox";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { Item, MaybeDeletedItem, isDeleted } from "@notesnook/core";
|
||||
import { debounce, debounceWithId } from "@notesnook/common";
|
||||
@@ -77,14 +79,13 @@ import { onPageVisibilityChanged } from "../../utils/page-visibility";
|
||||
import { Pane, SplitPane } from "../split-pane";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
import { isMobile } from "../../hooks/use-mobile";
|
||||
import { isTablet } from "../../hooks/use-tablet";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
const autoSaveToast = { show: true, hide: () => {} };
|
||||
|
||||
async function saveContent(
|
||||
export async function saveContent(
|
||||
noteId: string,
|
||||
ignoreEdit: boolean,
|
||||
content: string
|
||||
@@ -572,24 +573,7 @@ export function Editor(props: EditorProps) {
|
||||
const { hash, type } = data;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (attachment && type === "image") {
|
||||
const container = document.getElementById("dialogContainer");
|
||||
if (!(container instanceof HTMLElement)) return;
|
||||
|
||||
const dataurl = await downloadAttachment(hash, "base64", id);
|
||||
if (!dataurl)
|
||||
return showToast("error", strings.imagePreviewFailed());
|
||||
|
||||
ReactDOM.render(
|
||||
<ScopedThemeProvider>
|
||||
<Lightbox
|
||||
image={dataurl}
|
||||
onClose={() => {
|
||||
ReactDOM.unmountComponentAtNode(container);
|
||||
}}
|
||||
/>
|
||||
</ScopedThemeProvider>,
|
||||
container
|
||||
);
|
||||
await previewImageAttachment(attachment);
|
||||
} else if (
|
||||
attachment &&
|
||||
onPreviewDocument &&
|
||||
@@ -711,19 +695,25 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
||||
const child = editorContainerRef.current?.getBoundingClientRect();
|
||||
if (!parent || !child || !editor || entries.length <= 0) return;
|
||||
|
||||
const CONTAINER_MARGIN = isMobile() || isTablet() ? 10 : 30;
|
||||
const CONTAINER_MARGIN = isMobile() ? 10 : 30;
|
||||
const negativeSpace = Math.abs(
|
||||
parent.left - child.left - CONTAINER_MARGIN
|
||||
);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!isMobile() && !isTablet()) {
|
||||
if (!isMobile()) {
|
||||
editor.style.marginLeft = `-${negativeSpace}px`;
|
||||
editor.style.marginRight = `-${negativeSpace}px`;
|
||||
|
||||
editor.style.paddingLeft = `${negativeSpace}px`;
|
||||
editor.style.paddingRight = `${negativeSpace}px`;
|
||||
}
|
||||
editor.style.paddingLeft = `${negativeSpace}px`;
|
||||
editor.style.paddingRight = `${negativeSpace}px`;
|
||||
});
|
||||
|
||||
const editorTitle = document.querySelector("#editor-title");
|
||||
if (editorTitle instanceof HTMLTextAreaElement) {
|
||||
resizeTextarea(editorTitle);
|
||||
}
|
||||
}
|
||||
const observer = new ResizeObserver(debounce(onResize, 500));
|
||||
observer.observe(editorScrollRef.current);
|
||||
@@ -761,8 +751,8 @@ function EditorChrome(props: PropsWithChildren<EditorProps>) {
|
||||
maxWidth: editorMargins ? "min(100%, 850px)" : "auto",
|
||||
width: "100%"
|
||||
}}
|
||||
pl={[2, 2, 6]}
|
||||
pr={[2, 2, 6]}
|
||||
pl={[2, 6]}
|
||||
pr={[2, 6]}
|
||||
onClick={onRequestFocus}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -24,9 +24,15 @@ import BaseStore from "../../stores";
|
||||
import type { TOCItem, ToolbarDefinition } from "@notesnook/editor";
|
||||
import Config from "../../utils/config";
|
||||
import { getCurrentPreset } from "../../common/toolbar-config";
|
||||
import { EDITOR_ZOOM } from "./common";
|
||||
import { EDITOR_LINE_HEIGHT, EDITOR_ZOOM } from "./common";
|
||||
|
||||
type EditorConfig = {
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
zoom: number;
|
||||
lineHeight: number;
|
||||
};
|
||||
|
||||
type EditorConfig = { fontFamily: string; fontSize: number; zoom: number };
|
||||
type EditorContext = {
|
||||
editor?: IEditor;
|
||||
canUndo?: boolean;
|
||||
@@ -41,9 +47,10 @@ class EditorManager extends BaseStore<EditorManager> {
|
||||
editorConfig: EditorConfig = Config.get("editorConfig", {
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: 14,
|
||||
zoom: EDITOR_ZOOM.DEFAULT
|
||||
zoom: EDITOR_ZOOM.DEFAULT,
|
||||
lineHeight: EDITOR_LINE_HEIGHT.DEFAULT
|
||||
});
|
||||
editors: Record<string, EditorContext> = {};
|
||||
editors: Record<string, EditorContext | undefined> = {};
|
||||
|
||||
getEditor = (id: string): EditorContext | undefined => {
|
||||
return this.get().editors[id];
|
||||
|
||||
@@ -201,7 +201,14 @@ function TableOfContents(props: TableOfContentsProps) {
|
||||
{item.hasChildren ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{ bg: "transparent", p: 0, borderRadius: 100 }}
|
||||
sx={{
|
||||
bg: "transparent",
|
||||
p: 0,
|
||||
borderRadius: 100,
|
||||
width: 14,
|
||||
height: 14,
|
||||
flexShrink: 0
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
expanded ? collapse() : expand();
|
||||
@@ -214,7 +221,17 @@ function TableOfContents(props: TableOfContentsProps) {
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<Circle size={5} color="icon-secondary" />
|
||||
<Flex
|
||||
sx={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<Circle size={5} color="icon-secondary" />
|
||||
</Flex>
|
||||
)}
|
||||
{item.data.title}
|
||||
</Button>
|
||||
|
||||
@@ -57,7 +57,7 @@ import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
import { DayFormat, parseInternalLink } from "@notesnook/core";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import useTablet from "../../hooks/use-tablet";
|
||||
@@ -68,6 +68,7 @@ import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -107,6 +108,7 @@ type TipTapProps = {
|
||||
doubleSpacedLines: boolean;
|
||||
dateFormat: string;
|
||||
timeFormat: TimeFormat;
|
||||
dayFormat: DayFormat;
|
||||
markdownShortcuts: boolean;
|
||||
fontLigatures: boolean;
|
||||
};
|
||||
@@ -182,6 +184,7 @@ function TipTap(props: TipTapProps) {
|
||||
doubleSpacedLines,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dayFormat,
|
||||
markdownShortcuts,
|
||||
fontLigatures
|
||||
} = props;
|
||||
@@ -191,7 +194,9 @@ function TipTap(props: TipTapProps) {
|
||||
const features = useAreFeaturesAvailable([
|
||||
"callout",
|
||||
"outlineList",
|
||||
"taskList"
|
||||
"taskList",
|
||||
"exportTableAsCsv",
|
||||
"importCsvToTable"
|
||||
]);
|
||||
|
||||
usePermissionHandler({
|
||||
@@ -199,7 +204,9 @@ function TipTap(props: TipTapProps) {
|
||||
callout: !!features?.callout?.isAllowed,
|
||||
outlineList: !!features?.outlineList?.isAllowed,
|
||||
taskList: !!features?.taskList?.isAllowed,
|
||||
insertAttachment: !!useUserStore.getState().isLoggedIn
|
||||
insertAttachment: !!useUserStore.getState().isLoggedIn,
|
||||
exportTableAsCsv: !!features?.exportTableAsCsv?.isAllowed,
|
||||
importCsvToTable: !!features?.importCsvToTable?.isAllowed
|
||||
},
|
||||
onPermissionDenied: (claim, silent) => {
|
||||
if (claim === "insertAttachment") {
|
||||
@@ -261,6 +268,7 @@ function TipTap(props: TipTapProps) {
|
||||
doubleSpacedLines,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dayFormat,
|
||||
element: editorContainer(),
|
||||
editable: !readonly,
|
||||
content: content?.(),
|
||||
@@ -430,6 +438,7 @@ function TipTap(props: TipTapProps) {
|
||||
doubleSpacedLines,
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
dayFormat,
|
||||
markdownShortcuts,
|
||||
fontLigatures
|
||||
]);
|
||||
@@ -535,6 +544,7 @@ function TiptapWrapper(
|
||||
| "doubleSpacedLines"
|
||||
| "dateFormat"
|
||||
| "timeFormat"
|
||||
| "dayFormat"
|
||||
| "markdownShortcuts"
|
||||
| "fontLigatures"
|
||||
>
|
||||
@@ -551,6 +561,7 @@ function TiptapWrapper(
|
||||
);
|
||||
const dateFormat = useSettingsStore((store) => store.dateFormat);
|
||||
const timeFormat = useSettingsStore((store) => store.timeFormat);
|
||||
const dayFormat = useSettingsStore((store) => store.dayFormat);
|
||||
const markdownShortcuts = useSettingsStore(
|
||||
(store) => store.markdownShortcuts
|
||||
);
|
||||
@@ -623,19 +634,21 @@ function TiptapWrapper(
|
||||
".tiptap.ProseMirror": { pb: 150 },
|
||||
".editor-container": {
|
||||
opacity: isHydrating ? 0 : 1,
|
||||
zoom: editorConfig.zoom + "%"
|
||||
zoom: editorConfig.zoom + "%",
|
||||
lineHeight: editorConfig.lineHeight
|
||||
},
|
||||
".editor-loading-container.hidden": { display: "none" }
|
||||
}}
|
||||
>
|
||||
<TipTap
|
||||
key={`tiptap-${props.id}-${doubleSpacedLines}-${dateFormat}-${timeFormat}-${markdownShortcuts}-${fontLigatures}`}
|
||||
key={`tiptap-${props.id}-${doubleSpacedLines}-${dateFormat}-${timeFormat}-${dayFormat}-${markdownShortcuts}-${fontLigatures}`}
|
||||
{...props}
|
||||
isMobile={isMobile}
|
||||
isTablet={isTablet}
|
||||
doubleSpacedLines={doubleSpacedLines}
|
||||
dateFormat={dateFormat}
|
||||
timeFormat={timeFormat}
|
||||
dayFormat={dayFormat}
|
||||
markdownShortcuts={markdownShortcuts}
|
||||
fontLigatures={fontLigatures}
|
||||
onLoad={(editor) => {
|
||||
|
||||
@@ -155,7 +155,7 @@ export default React.memo(TitleBox, (prevProps, nextProps) => {
|
||||
return prevProps.readonly === nextProps.readonly;
|
||||
});
|
||||
|
||||
function resizeTextarea(input: HTMLTextAreaElement) {
|
||||
export function resizeTextarea(input: HTMLTextAreaElement) {
|
||||
input.style.height = "auto";
|
||||
requestAnimationFrame(() => {
|
||||
input.style.height = input.scrollHeight + "px";
|
||||
|
||||
@@ -223,15 +223,12 @@ import {
|
||||
mdiNoteRemoveOutline,
|
||||
mdiTabPlus,
|
||||
mdiRadar,
|
||||
mdiLinkBoxOutline,
|
||||
mdiHistory,
|
||||
mdiArrowCollapseLeft,
|
||||
mdiArrowCollapseRight,
|
||||
mdiHamburger,
|
||||
mdiNotePlus,
|
||||
mdiNoteEditOutline,
|
||||
mdiArrowUp,
|
||||
mdiInbox
|
||||
mdiInbox,
|
||||
mdiConsoleLine
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -456,6 +453,7 @@ export const ImageDownload = createIcon(mdiImage);
|
||||
export const Billboard = createIcon(mdiBillboard);
|
||||
export const Cellphone = createIcon(mdiCellphone);
|
||||
export const CellphoneLock = createIcon(mdiCellphoneLock);
|
||||
export const ConsoleLine = createIcon(mdiConsoleLine);
|
||||
export const FileLock = createIcon(mdiFileLockOutline);
|
||||
export const ShieldLock = createIcon(mdiShieldLockOutline);
|
||||
export const ImageMultiple = createIcon(mdiImageMultipleOutline);
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {
|
||||
NoteResolvedData,
|
||||
areFeaturesAvailable,
|
||||
exportContent,
|
||||
getFormattedDate,
|
||||
getFormattedReminderTime
|
||||
@@ -66,9 +67,12 @@ import {
|
||||
Attachment,
|
||||
AttachmentError,
|
||||
Circle,
|
||||
Close,
|
||||
Colors,
|
||||
Copy,
|
||||
Destruct,
|
||||
Duplicate,
|
||||
Edit,
|
||||
Export,
|
||||
HTML,
|
||||
InternalLink,
|
||||
@@ -98,6 +102,8 @@ import { Context } from "../list-container/types";
|
||||
import ListItem from "../list-item";
|
||||
import { PublishDialog } from "../publish-view";
|
||||
import TimeAgo from "../time-ago";
|
||||
import { NoteExpiryDateDialog } from "../../dialogs/note-expiry-date-dialog";
|
||||
import { withFeatureCheck } from "../../common";
|
||||
|
||||
type NoteProps = NoteResolvedData & {
|
||||
item: NoteType;
|
||||
@@ -179,6 +185,8 @@ function Note(props: NoteProps) {
|
||||
{locked && <Lock size={11} data-test-id={`locked`} />}
|
||||
{note.favorite && <Star color={primary} size={15} />}
|
||||
{note.readonly && <Readonly size={15} />}
|
||||
{note.expiryDate?.value ? <Destruct size={13} /> : null}
|
||||
|
||||
<TimeAgo live={true} datetime={date} locale="short" />
|
||||
</>
|
||||
) : (
|
||||
@@ -268,6 +276,13 @@ function Note(props: NoteProps) {
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{note.expiryDate?.value && (
|
||||
<IconTag
|
||||
icon={Destruct}
|
||||
text={getFormattedDate(note.expiryDate.value, "date")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -289,7 +304,8 @@ export default React.memo(Note, function (prevProps, nextProps) {
|
||||
prevProps.attachments?.failed === nextProps.attachments?.failed &&
|
||||
prevProps.attachments?.total === nextProps.attachments?.total &&
|
||||
prevProps.locked === nextProps.locked &&
|
||||
prevProps.color?.id === nextProps.color?.id
|
||||
prevProps.color?.id === nextProps.color?.id &&
|
||||
prevItem.expiryDate?.value === nextItem.expiryDate?.value
|
||||
);
|
||||
});
|
||||
|
||||
@@ -330,9 +346,9 @@ export const noteMenuItems: (
|
||||
note: NoteType,
|
||||
ids?: string[],
|
||||
context?: { color?: Color; locked?: boolean }
|
||||
) => MenuItem[] = (note, ids = [], context) => {
|
||||
) => Promise<MenuItem[]> = async (note, ids = [], context) => {
|
||||
// const isSynced = db.notes.note(note.id)?.synced();
|
||||
|
||||
const features = await areFeaturesAvailable(["expiringNotes"]);
|
||||
return [
|
||||
{
|
||||
type: "button",
|
||||
@@ -603,6 +619,52 @@ export const noteMenuItems: (
|
||||
multiSelect: true
|
||||
},
|
||||
{ key: "sep3", type: "separator" },
|
||||
note.expiryDate?.value
|
||||
? {
|
||||
type: "button",
|
||||
key: "expiry-date",
|
||||
title: strings.expiryDate(),
|
||||
icon: Destruct.path,
|
||||
menu: {
|
||||
items: [
|
||||
{
|
||||
type: "button",
|
||||
key: "change",
|
||||
title: strings.change(),
|
||||
onClick: async () => {
|
||||
await NoteExpiryDateDialog.show({
|
||||
noteId: note.id,
|
||||
expiryDate: note.expiryDate?.value
|
||||
});
|
||||
},
|
||||
icon: Edit.path
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "remove",
|
||||
title: strings.remove(),
|
||||
onClick: async () => {
|
||||
await db.notes.setExpiryDate(null, ...ids);
|
||||
store.refresh();
|
||||
showToast("success", "Expiry date removed");
|
||||
},
|
||||
icon: Close.path
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
: {
|
||||
type: "button",
|
||||
key: "expiry-date",
|
||||
title: strings.setExpiry(),
|
||||
icon: Destruct.path,
|
||||
premium: !features.expiringNotes.isAllowed,
|
||||
onClick: async () => {
|
||||
await NoteExpiryDateDialog.show({
|
||||
noteId: note.id
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "movetotrash",
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
Unlock,
|
||||
Readonly,
|
||||
SyncOff,
|
||||
ArrowLeft,
|
||||
Circle,
|
||||
Checkmark,
|
||||
ChevronDown,
|
||||
@@ -32,9 +31,10 @@ import {
|
||||
LinkedTo,
|
||||
ReferencedIn as ReferencedInIcon,
|
||||
Note as NoteIcon,
|
||||
Archive
|
||||
Archive,
|
||||
Edit
|
||||
} from "../icons";
|
||||
import { Box, Button, Flex, Text, FlexProps } from "@theme-ui/components";
|
||||
import { Button, Flex, Text, FlexProps } from "@theme-ui/components";
|
||||
import {
|
||||
useEditorStore,
|
||||
ReadonlyEditorSession,
|
||||
@@ -45,12 +45,12 @@ import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useAttachmentStore } from "../../stores/attachment-store";
|
||||
import { store as noteStore } from "../../stores/note-store";
|
||||
import Toggle from "./toggle";
|
||||
import { EditNoteCreationDateDialog } from "../../dialogs/edit-note-creation-date-dialog";
|
||||
import ScrollContainer from "../scroll-container";
|
||||
import {
|
||||
getFormattedDate,
|
||||
usePromise,
|
||||
ResolvedItem,
|
||||
useResolvedItem,
|
||||
useUnresolvedItem
|
||||
} from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
@@ -59,7 +59,6 @@ import { VirtualizedList } from "../virtualized-list";
|
||||
import { SessionItem } from "../session-item";
|
||||
import {
|
||||
ContentBlock,
|
||||
InternalLink,
|
||||
Note,
|
||||
VirtualizedGrouping,
|
||||
createInternalLink,
|
||||
@@ -67,7 +66,6 @@ import {
|
||||
} from "@notesnook/core";
|
||||
import { VirtualizedTable } from "../virtualized-table";
|
||||
import { TextSlice } from "@notesnook/core";
|
||||
import { TITLE_BAR_HEIGHT } from "../title-bar";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const tools = [
|
||||
@@ -212,13 +210,39 @@ function EditorProperties(props: EditorPropertiesProps) {
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text
|
||||
className="selectable"
|
||||
variant="subBody"
|
||||
sx={{ fontSize: "body", flexShrink: 0 }}
|
||||
>
|
||||
{item.value(session.note[item.key])}
|
||||
</Text>
|
||||
|
||||
{item.key === "dateCreated" ? (
|
||||
<Flex sx={{ alignItems: "center", gap: 1 }}>
|
||||
<Text
|
||||
data-test-id="date-created"
|
||||
className="selectable"
|
||||
variant="subBody"
|
||||
sx={{ fontSize: "body", flexShrink: 0 }}
|
||||
>
|
||||
{item.value(session.note[item.key])}
|
||||
</Text>
|
||||
<Edit
|
||||
size={14}
|
||||
sx={{ cursor: "pointer", color: "icon" }}
|
||||
onClick={() => {
|
||||
EditNoteCreationDateDialog.show({
|
||||
noteId: session.note.id,
|
||||
dateCreated: session.note.dateCreated,
|
||||
dateEdited: session.note.dateEdited
|
||||
});
|
||||
}}
|
||||
data-test-id="edit-date-created"
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<Text
|
||||
className="selectable"
|
||||
variant="subBody"
|
||||
sx={{ fontSize: "body", flexShrink: 0 }}
|
||||
>
|
||||
{item.value(session.note[item.key])}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
{session.type === "deleted" ||
|
||||
|
||||
@@ -56,6 +56,7 @@ function PublishView(props: PublishViewProps) {
|
||||
current: number;
|
||||
}>();
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
const titleInput = useRef<HTMLInputElement>(null);
|
||||
const publishNote = useStore((store) => store.publish);
|
||||
const unpublishNote = useStore((store) => store.unpublish);
|
||||
const [monograph, setMonograph] = useState(props.monograph);
|
||||
@@ -125,6 +126,26 @@ function PublishView(props: PublishViewProps) {
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
px: 1,
|
||||
height: 30,
|
||||
|
||||
"& label": { width: "auto", flexShrink: 0 }
|
||||
}}
|
||||
>
|
||||
<Text variant="body">{strings.title()}</Text>
|
||||
<Input
|
||||
ref={titleInput}
|
||||
type="text"
|
||||
variant="clean"
|
||||
placeholder={strings.enterTitle()}
|
||||
defaultValue={monograph ? monograph.title : note.title}
|
||||
sx={{ textAlign: "right", p: 0 }}
|
||||
/>
|
||||
</Flex>
|
||||
{monograph?.publishedAt ? (
|
||||
<Flex
|
||||
sx={{
|
||||
@@ -286,8 +307,14 @@ function PublishView(props: PublishViewProps) {
|
||||
try {
|
||||
setStatus({ action: "publish" });
|
||||
const password = passwordInput.current?.value;
|
||||
const title = titleInput.current?.value;
|
||||
|
||||
await publishNote(note.id, {
|
||||
if (!title || title.trim().length === 0) {
|
||||
showToast("error", "Title cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
await publishNote(note.id, title, {
|
||||
selfDestruct,
|
||||
password
|
||||
});
|
||||
@@ -425,6 +452,7 @@ type ResolvedMonograph = {
|
||||
selfDestruct: boolean;
|
||||
publishedAt?: number;
|
||||
password?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
async function resolveMonograph(
|
||||
@@ -436,6 +464,7 @@ async function resolveMonograph(
|
||||
id: monographId,
|
||||
selfDestruct: !!monograph.selfDestruct,
|
||||
publishedAt: monograph.datePublished,
|
||||
title: monograph.title,
|
||||
password: monograph.password
|
||||
? await db.monographs.decryptPassword(monograph.password)
|
||||
: undefined
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
SyncOff,
|
||||
Icon,
|
||||
Unlock,
|
||||
CellphoneLock
|
||||
CellphoneLock,
|
||||
ConsoleLine
|
||||
} from "../icons";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
@@ -45,6 +46,7 @@ import { strings } from "@notesnook/intl";
|
||||
import { useVault } from "../../hooks/use-vault";
|
||||
import { useKeyStore } from "../../interfaces/key-store";
|
||||
import { STATUS_BAR_HEIGHT } from "../../common/constants";
|
||||
import { CommandPaletteDialog } from "../../dialogs/command-palette";
|
||||
|
||||
function StatusBar() {
|
||||
const user = useUserStore((state) => state.user);
|
||||
@@ -73,6 +75,20 @@ function StatusBar() {
|
||||
<Flex />
|
||||
) : (
|
||||
<Flex sx={{ gap: "small" }}>
|
||||
<Button
|
||||
variant="statusitem"
|
||||
onClick={() => CommandPaletteDialog.show({ isCommandMode: true })}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
display: "flex",
|
||||
color: "paragraph",
|
||||
height: "100%"
|
||||
}}
|
||||
title={"Open command palette"}
|
||||
>
|
||||
<ConsoleLine size={12} />
|
||||
</Button>
|
||||
{isLoggedIn ? (
|
||||
<>
|
||||
{user?.isEmailConfirmed ? (
|
||||
|
||||
@@ -25,6 +25,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import { useRef, useState } from "react";
|
||||
import { db } from "../common/db";
|
||||
import { useStore } from "../stores/reminder-store";
|
||||
import { useStore as useSettingsStore } from "../stores/setting-store";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { Calendar, Pro } from "../components/icons";
|
||||
import { usePersistentState } from "../hooks/use-persistent-state";
|
||||
@@ -37,6 +38,7 @@ import { Note, Reminder } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { checkFeature } from "../common";
|
||||
import { setTimeOnly, setDateOnly } from "../utils/date-time";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
@@ -67,6 +69,8 @@ const RecurringModes = {
|
||||
} as const;
|
||||
|
||||
const WEEK_DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
const WEEK_DAYS_MON = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
const modes = [
|
||||
{
|
||||
id: Modes.ONCE,
|
||||
@@ -118,6 +122,8 @@ export const AddReminderDialog = DialogManager.register(
|
||||
function AddReminderDialog(props: AddReminderDialogProps) {
|
||||
const { reminder, note } = props;
|
||||
|
||||
const weekFormat = useSettingsStore((store) => store.weekFormat);
|
||||
const weekDays = weekFormat === "Sun" ? WEEK_DAYS : WEEK_DAYS_MON;
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>(
|
||||
reminder?.selectedDays ?? []
|
||||
);
|
||||
@@ -348,7 +354,7 @@ export const AddReminderDialog = DialogManager.register(
|
||||
: "paragraph"
|
||||
}}
|
||||
>
|
||||
{mode.id === "week" ? WEEK_DAYS[i] : day}
|
||||
{mode.id === "week" ? weekDays[i] : day}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
@@ -497,13 +503,13 @@ export const AddReminderDialog = DialogManager.register(
|
||||
: strings.reminderRepeatStrings.repeats(
|
||||
1,
|
||||
recurringMode,
|
||||
getSelectedDaysText(selectedDays, recurringMode),
|
||||
getSelectedDaysText(selectedDays, recurringMode, weekDays),
|
||||
date.format(timeFormat())
|
||||
)}
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
{strings.reminderStarts(date.toString(), date.format(timeFormat()))}
|
||||
{strings.reminderStarts(date.format(db.settings.getDateFormat()), date.format(timeFormat()))}
|
||||
</Text>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -515,23 +521,14 @@ export const AddReminderDialog = DialogManager.register(
|
||||
}
|
||||
);
|
||||
|
||||
function setTimeOnly(str: string, date: dayjs.Dayjs) {
|
||||
const value = dayjs(str, timeFormat(), true);
|
||||
return date.hour(value.hour()).minute(value.minute());
|
||||
}
|
||||
|
||||
function timeFormat() {
|
||||
return getTimeFormat(db.settings.getTimeFormat());
|
||||
}
|
||||
|
||||
function setDateOnly(str: string, date: dayjs.Dayjs) {
|
||||
const value = dayjs(str, db.settings.getDateFormat(), true);
|
||||
return date.year(value.year()).month(value.month()).date(value.date());
|
||||
}
|
||||
|
||||
function getSelectedDaysText(
|
||||
selectedDays: number[],
|
||||
recurringMode: ValueOf<typeof RecurringModes>
|
||||
recurringMode: ValueOf<typeof RecurringModes>,
|
||||
weekDays: typeof WEEK_DAYS | typeof WEEK_DAYS_MON
|
||||
) {
|
||||
const text = selectedDays
|
||||
.sort((a, b) => a - b)
|
||||
@@ -540,7 +537,7 @@ function getSelectedDaysText(
|
||||
const isSecondLast = index === selectedDays.length - 2;
|
||||
const joinWith = isSecondLast ? " & " : isLast ? "" : ", ";
|
||||
return recurringMode === RecurringModes.WEEK
|
||||
? WEEK_DAYS[day] + joinWith
|
||||
? weekDays[day] + joinWith
|
||||
: `${day}${nth(day)} ${joinWith}`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -328,7 +328,7 @@ async function getActiveNoteCommands(): Promise<Command[]> {
|
||||
|
||||
const menuItems =
|
||||
note.type !== "trash"
|
||||
? noteMenuItems(note, [note.id], {
|
||||
? await noteMenuItems(note, [note.id], {
|
||||
locked: !!(
|
||||
await db
|
||||
.sql()
|
||||
@@ -432,7 +432,7 @@ function getEditorCommands(): Command[] {
|
||||
}
|
||||
];
|
||||
|
||||
if (session.type !== "readonly" && (editor.canUndo || editor.canRedo)) {
|
||||
if (session.type !== "readonly" && (editor?.canUndo || editor?.canRedo)) {
|
||||
commands.push(
|
||||
{
|
||||
id: "undo",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user