Compare commits

..

4 Commits

Author SHA1 Message Date
Abdullah Atta
f2efa56bfc core: fix notes showing after serach even though no color is assigned 2026-07-31 11:51:50 +05:00
Abdullah Atta
9fdc8a9f3e core: fix lookup failing when searching notes with colors having same name 2026-07-30 18:26:15 +05:00
Abdullah Atta
92a04e2aa7 web: show "no results found" error 2026-07-30 09:18:39 +05:00
Abdullah Atta
db8408f62f core: fix notes lookup with non-existent tags 2026-07-25 09:02:59 +05:00
556 changed files with 4277 additions and 24389 deletions

View File

@@ -1,79 +0,0 @@
name: Notesnook Help PR Preview
on:
pull_request:
types: [opened, reopened, synchronize]
branches: [master, beta]
paths:
- "docs/help/**"
# re-run workflow if workflow file changes
- ".github/workflows/help.preview.yml"
jobs:
build-and-deploy:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install dependencies
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=help
- name: Build help
run: npm run build:help
- name: Deploy to Cloudflare
id: deploy
working-directory: ./docs/help
run: |
set -euo pipefail
DEPLOY_OUT=$(npx --yes wrangler versions upload 2>&1) || { echo "$DEPLOY_OUT"; exit 1; }
echo "$DEPLOY_OUT"
PREVIEW_URL=$(printf "%s" "$DEPLOY_OUT" | grep -Eo 'https?://[^ ]+' | head -1 || true)
if [ -z "$PREVIEW_URL" ]; then
echo "WARNING: could not parse preview URL from wrangler output"
fi
echo "preview_url=$PREVIEW_URL" >> $GITHUB_ENV
- name: Post or update PR comment
uses: actions/github-script@v6
env:
preview_url: ${{ env.preview_url }}
with:
script: |
const marker = '<!-- docs-pages-preview-comment -->';
const prNumber = context.issue.number;
const previewUrl = process.env.preview_url || '';
const body = `${marker}\n**Cloudflare Pages Docs Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}

View File

@@ -2,6 +2,11 @@ name: Publish Notesnook Help
on:
workflow_dispatch:
push:
branches:
- "master"
paths:
- "docs/help/**"
jobs:
build:
@@ -9,21 +14,17 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- uses: actions-rs/toolchain@v1
with:
# VitePress reads git history to show the last updated date per page.
fetch-depth: 0
toolchain: stable
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
- name: Install docgen
run: cargo install --git https://github.com/thecodrr/docgen
- name: Install dependencies
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=help
- name: Build help
run: npm run build:help
- name: Build site
run: docgen build --release
working-directory: docs/help
- name: Setup environment
run: |
@@ -31,4 +32,4 @@ jobs:
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV
- name: Publish on Cloudflare Pages
run: npx --yes wrangler deploy
run: npx --yes wrangler pages deploy --project-name notesnook-help ./docs/help/site/ --branch main

View File

@@ -27,9 +27,7 @@ jobs:
uses: ./.github/actions/setup-node-with-cache
- name: Install dependencies
run: |
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=web
run: npm ci
- name: Build web
run: npm run build:web

View File

@@ -65,7 +65,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
## Additional Resources
- [Migrating & Importing your data from other apps — Importer](https://notesnook.com/help/importing-notes)
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)

View File

@@ -42,7 +42,6 @@ export interface AppContext {
export interface TestOptions {
version: string;
args?: string[];
config?: Record<string, unknown>;
}
@@ -73,8 +72,7 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version,
options?.args
options?.version
);
const ctx: AppContext = {
app,
@@ -84,8 +82,7 @@ export async function buildAndLaunchApp(
const { app } = await launchApp(
executablePath,
userDataDir,
options?.version,
options?.args
options?.version
);
ctx.app = app;
ctx.userDataDir = userDataDir;
@@ -97,12 +94,11 @@ export async function buildAndLaunchApp(
async function launchApp(
executablePath: string,
userDataDir: string,
version?: string,
args: string[] = []
version?: string
) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [...args] : ["--hidden", ...args],
args: IS_DEBUG ? [] : ["--hidden"],
baseURL: "https://app.notesnook.com",
acceptDownloads: true,
env: {
@@ -158,7 +154,6 @@ export async function buildApp(version?: string) {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
CSC_IDENTITY_AUTO_DISCOVERY: "false",
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,

View File

@@ -17,65 +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 { test, expect } from "@nn/test";
import type { ElectronApplication } from "@playwright/test";
async function getMainWindowState(app: ElectronApplication) {
return await app.evaluate((window) => {
const { BrowserWindow } = window;
const mainWindow = BrowserWindow.getAllWindows()[0];
if (!mainWindow) throw new Error("Main window not found");
return {
isMinimized: mainWindow.isMinimized(),
isVisible: mainWindow.isVisible()
};
});
}
import { test } from "@nn/test";
test("make sure app loads", async ({ page }) => {
await page.waitForSelector(".ProseMirror");
});
test("hidden launch minimizes when tray is disabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: false
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(true);
});
test("hidden launch does not minimize when close-to-tray is enabled", async ({
launchElectronApp,
options
}) => {
const app = await launchElectronApp({
version: options.version,
args: ["--hidden"],
config: {
desktopSettings: {
minimizeToSystemTray: false,
closeToSystemTray: true
}
}
});
const page = await app.firstWindow();
await page.waitForSelector(".ProseMirror");
const state = await getMainWindowState(app);
expect(state.isMinimized).toBe(false);
});

View File

@@ -203,12 +203,11 @@ module.exports = {
toolsets: {
appimage: "1.0.2"
},
snapcraft: {
base: "core24",
core24: {
confinement: "strict",
autoStart: false
}
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true,
base: "core22"
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -92,17 +92,6 @@ async function createWindow() {
const cliOptions = await parseArguments(process.argv);
setTheme(getTheme());
// this workaround is necessary because macos doesn't support
// the --hidden flag when launching the app on startup
if (
process.platform === "darwin" &&
app.getLoginItemSettings().wasOpenedAtLogin &&
config.desktopSettings.autoStart &&
config.desktopSettings.startMinimized
) {
cliOptions.hidden = true;
}
const mainWindowState = new WindowState({});
const mainWindow = new BrowserWindow({
show: !cliOptions.hidden,
@@ -152,13 +141,8 @@ async function createWindow() {
mainWindow.setMenuBarVisibility(false);
mainWindowState.manage(mainWindow);
if (
cliOptions.hidden &&
!(
config.desktopSettings.minimizeToSystemTray ||
config.desktopSettings.closeToSystemTray
)
)
if (cliOptions.hidden && !(config.desktopSettings.minimizeToSystemTray
|| config.desktopSettings.closeToSystemTray))
mainWindow.minimize();
await mainWindow.webContents.loadURL(`${createURL(cliOptions, "/")}`);

View File

@@ -140,7 +140,7 @@ android {
if (project.hasProperty("prBuildNumber")) {
versionCode Integer.parseInt(prBuildNumber())
} else {
versionCode 3116
versionCode 3113
}
versionName getNpmVersion()
testBuildType System.getProperty('testBuildType', 'debug')
@@ -233,9 +233,6 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
// Lets the widgets push their list data straight into the RemoteViews on every API level,
// instead of the deprecated RemoteViewsService adapter (which needs API 31 to do natively).
implementation("androidx.core:core-remoteviews:1.0.0")
implementation("androidx.core:core-splashscreen:1.0.0")
implementation 'androidx.multidex:multidex:2.0.1'

View File

@@ -86,9 +86,8 @@
android:name=".NotePreviewWidget"
android:exported="false"
android:label="@string/note">
<intent-filter>
<intent-filter>"
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="android.appwidget.action.APPWIDGET_RESTORED" />
</intent-filter>
<meta-data
@@ -96,15 +95,6 @@
android:resource="@xml/note_widget_info" />
</receiver>
<receiver
android:name=".WidgetTimeChangeReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.TIME_SET" />
<action android:name="android.intent.action.TIMEZONE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".ReminderWidgetProvider"
android:exported="false"
@@ -240,6 +230,11 @@
</intent-filter>
</service>
<service
android:name=".ReminderViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@@ -5,11 +5,15 @@ import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViews;
import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
import com.facebook.react.defaults.DefaultReactActivityDelegate;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
public class NotePreviewConfigureActivity extends ReactActivity {
@@ -36,32 +40,18 @@ public class NotePreviewConfigureActivity extends ReactActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
activity = this;
readAppWidgetId(getIntent());
}
/**
* We launch as singleTask, so configuring a second widget while this screen is still alive
* arrives here rather than in onCreate(). Without this the activity would keep writing to
* whichever widget it happened to be opened for first.
*/
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
activity = this;
readAppWidgetId(intent);
}
private void readAppWidgetId(Intent intent) {
Bundle extras = intent != null ? intent.getExtras() : null;
int appWidgetId = extras != null
? extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
: AppWidgetManager.INVALID_APPWIDGET_ID;
NotePreviewConfigureActivity.appWidgetId = appWidgetId;
Intent intent = getIntent();
Bundle extras = intent.getExtras();
int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
NotePreviewConfigureActivity.appWidgetId = appWidgetId;
}
Intent resultValue = new Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(Activity.RESULT_CANCELED, resultValue);
activity = this;
}
public static void saveAndFinish(Context context) {

View File

@@ -1,5 +1,6 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
@@ -7,125 +8,51 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.RemoteViews;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
import java.util.HashSet;
import java.util.Set;
public class NotePreviewWidget extends AppWidgetProvider {
static String OpenNoteId = "com.streetwriters.notesnook.OpenNoteId";
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
String data = context.getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
Note note = WidgetUtils.parseNote(data);
if (note == null) {
// Either the widget was never configured, or we lost the note it pointed at (ids
// reassigned, data cleared). Point it back at the picker rather than leaving the user
// with an inert widget they can only fix by deleting and re-adding it.
views.setTextViewText(R.id.widget_title, context.getString(R.string.widget_note_unconfigured_title));
views.setTextViewText(R.id.widget_body, context.getString(R.string.widget_note_unconfigured_body));
views.setOnClickPendingIntent(R.id.open_note, getConfigurePendingIntent(context, appWidgetId));
appWidgetManager.updateAppWidget(appWidgetId, views);
String data = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE).getString(String.valueOf(appWidgetId), "");
if (data.isEmpty()) {
return;
}
Gson gson = new Gson();
Note note = gson.fromJson(data, Note.class);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.note_widget);
views.setTextViewText(R.id.widget_title, note.getTitle());
views.setTextViewText(R.id.widget_body, note.getHeadline());
// Once the user shrinks the widget down to a single row there is no room for the preview
// text, and a clipped half-line of it looks like a rendering glitch.
views.setViewVisibility(R.id.widget_body,
hasRoomForBody(appWidgetManager, appWidgetId) ? View.VISIBLE : View.GONE);
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra(OpenNoteId, note.getId());
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
intent.setData(Uri.parse("nn://note/" + note.getId()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
/**
* Reopens the configure screen for this widget. The launcher's own "reconfigure" gesture is
* hard to discover and not offered by every launcher, so an unconfigured widget needs its own
* way back in.
*/
private static PendingIntent getConfigurePendingIntent(Context context, int appWidgetId) {
Intent intent = new Intent(context, NotePreviewConfigureActivity.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
// PendingIntent equality ignores extras, so the widget id has to be the request code for
// each widget to get its own.
return PendingIntent.getActivity(context, appWidgetId, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE,
WidgetUtils.getActivityOptionsBundle());
}
/**
* Height below which the note preview text is dropped, leaving just the title.
*/
private static final int MIN_HEIGHT_FOR_BODY_DP = 70;
private static boolean hasRoomForBody(AppWidgetManager appWidgetManager, int appWidgetId) {
Bundle options = appWidgetManager.getAppWidgetOptions(appWidgetId);
if (options == null) return true;
int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
// Not reported yet (the widget has just been placed): assume there is room.
return minHeight <= 0 || minHeight >= MIN_HEIGHT_FOR_BODY_DP;
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
// This used to do nothing at all, so resizing the widget left it rendered for its old size.
updateAppWidget(context, appWidgetManager, appWidgetId);
}
/**
* The note shown by each widget is stored in the "appPreview" preferences under its widget id.
* When the system restores our widgets it hands out fresh ids, so unless we move the stored
* notes over to the new ids the widgets are left permanently blank with no way to recover
* other than removing and re-adding them.
*
* AppWidgetProvider calls onUpdate() with the new ids right after this, which re-renders them.
*/
@Override
public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) {
super.onRestored(context, oldWidgetIds, newWidgetIds);
if (oldWidgetIds == null || newWidgetIds == null) return;
int count = Math.min(oldWidgetIds.length, newWidgetIds.length);
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
// Read everything up front: an old id can collide with the new id of another widget.
String[] notes = new String[count];
Set<String> newKeys = new HashSet<>();
for (int i = 0; i < count; i++) {
notes[i] = preferences.getString(String.valueOf(oldWidgetIds[i]), "");
newKeys.add(String.valueOf(newWidgetIds[i]));
}
SharedPreferences.Editor edit = preferences.edit();
for (int i = 0; i < count; i++) {
String oldKey = String.valueOf(oldWidgetIds[i]);
if (!newKeys.contains(oldKey)) {
edit.remove(oldKey);
}
}
for (int i = 0; i < count; i++) {
if (notes[i].isEmpty()) continue;
edit.putString(String.valueOf(newWidgetIds[i]), notes[i]);
}
edit.apply();
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return activityOptions.toBundle();
} else
return null;
}
@Override

View File

@@ -1,10 +1,12 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.widget.RemoteViews;
@@ -22,10 +24,20 @@ public class NoteWidget extends AppWidgetProvider {
static void setClickIntent(Context context, RemoteViews views) {
Intent intent = new Intent(context, ShareActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.new_note, pendingIntent);
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return activityOptions.toBundle();
} else
return null;
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);

View File

@@ -12,6 +12,7 @@ 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;
@@ -28,6 +29,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.google.gson.Gson;
import com.streetwriters.notesnook.datatypes.Note;
import java.util.ArrayList;
@@ -136,7 +138,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
if (Objects.equals(extras.getString(IntentType), "NewReminder")) {
map.putString(ReminderWidgetProvider.NewReminder, extras.getString(ReminderWidgetProvider.NewReminder));
} else if (Objects.equals(extras.getString(IntentType), "OpenReminder")) {
map.putString(ReminderWidgetProvider.OpenReminderId, extras.getString(ReminderWidgetProvider.OpenReminderId));
map.putString(ReminderViewsService.OpenReminderId, extras.getString(ReminderViewsService.OpenReminderId));
} else if (Objects.equals(extras.getString(IntentType), "OpenNote")) {
map.putString(NotePreviewWidget.OpenNoteId, extras.getString(NotePreviewWidget.OpenNoteId));
}
@@ -154,8 +156,14 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getWidgetNotes(Promise promise) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
WritableArray arr = Arguments.createArray();
for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) {
for(Map.Entry<String,?> entry : map.entrySet()){
if (entry.getKey().equals("remindersList")) continue;
String value = (String) entry.getValue();
Gson gson = new Gson();
Note note = gson.fromJson(value, Note.class);
arr.pushString(note.getId());
}
promise.resolve(arr);
@@ -163,46 +171,36 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
@ReactMethod
public void hasWidgetNote(final String noteId, Promise promise) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
boolean found = false;
for (Note note : WidgetUtils.getWidgetNotes(getReactApplicationContext()).values()) {
if (note.getId().equals(noteId)) {
for(Map.Entry<String,?> entry : map.entrySet()){
String value = (String) entry.getValue();
if (value.contains(noteId)) {
found = true;
break;
}
}
promise.resolve(found);
}
@ReactMethod
public void updateWidgetNote(final String noteId, final String data) {
SharedPreferences pref = getReactApplicationContext().getSharedPreferences(WidgetUtils.PREFERENCES, Context.MODE_PRIVATE);
SharedPreferences pref = getReactApplicationContext().getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Map<String, ?> map = pref.getAll();
SharedPreferences.Editor edit = pref.edit();
List<Integer> ids = new ArrayList<>();
// Match on the note's id, not on the raw JSON containing it somewhere: a note whose body
// happens to mention another note's id is not the same note.
for (Map.Entry<Integer, Note> entry : WidgetUtils.getWidgetNotes(getReactApplicationContext()).entrySet()) {
if (!noteId.equals(entry.getValue().getId())) continue;
edit.putString(String.valueOf(entry.getKey()), data);
ids.add(entry.getKey());
ArrayList<String> ids = new ArrayList<>();
for(Map.Entry<String,?> entry : map.entrySet()) {
String value = (String) entry.getValue();
if (value.contains(noteId)) {
edit.putString(entry.getKey(), data);
ids.add(entry.getKey());
}
}
edit.apply();
for (int id : ids) {
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), id);
for (String id: ids) {
NotePreviewWidget.updateAppWidget(mContext, AppWidgetManager.getInstance(mContext), Integer.parseInt(id));
}
}
/**
* Redraws every widget from scratch. Needed because the app can be stopped while its widgets
* stay on the home screen: clearing app data empties the store without the widgets ever being
* told, so they keep showing content that is gone until something forces a redraw.
*/
@ReactMethod
public void refreshWidgets() {
WidgetUtils.refreshAll(mContext);
}
@ReactMethod
public void updateReminderWidget() {
AppWidgetManager wm = AppWidgetManager.getInstance(mContext);
@@ -210,8 +208,8 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
for (int id: ids) {
Log.d("Reminders", "Updating" + id);
RemoteViews views = new RemoteViews(mContext.getPackageName(), R.layout.widget_reminders);
// The rows are part of this update, so there is nothing left to invalidate afterwards.
ReminderWidgetProvider.updateAppWidget(mContext, wm, id, views);
wm.notifyAppWidgetViewDataChanged(id, R.id.widget_list_view);
}
}

View File

@@ -0,0 +1,106 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViewsService;
import android.content.Context;
import android.widget.RemoteViews;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.streetwriters.notesnook.datatypes.Reminder;
import java.util.ArrayList;
import java.util.List;
public class ReminderViewsService extends RemoteViewsService {
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ReminderRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class ReminderRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context context;
private List<Reminder> reminders;
public ReminderRemoteViewsFactory(Context context, Intent intent) {
this.context = context;
}
@Override
public void onCreate() {
// Initialize reminders list
reminders = new ArrayList<Reminder>();
}
@Override
public void onDataSetChanged() {
reminders.clear();
SharedPreferences preferences = context.getSharedPreferences("appPreview", Context.MODE_PRIVATE);
Gson gson = new Gson();
reminders = gson.fromJson(preferences.getString("remindersList","[]"), new TypeToken<List<Reminder>>(){}.getType());
}
@Override
public void onDestroy() {
reminders.clear();
}
@Override
public int getCount() {
return reminders.size();
}
@Override
public RemoteViews getViewAt(int position) {
Reminder reminder = reminders.get(position);
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
RemoteViews views = new RemoteViews(context.getPackageName(), useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
if (!useMiniLayout) {
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
}
views.setTextViewText(R.id.reminder_time, reminder.getFormattedTime());
final Intent fillInIntent = new Intent();
final Bundle extras = new Bundle();
extras.putString(ReminderViewsService.OpenReminderId, reminder.getId());
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
fillInIntent.putExtras(extras);
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
return views;
}
@Override
public RemoteViews getLoadingView() {
return null;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return true;
}
}

View File

@@ -1,22 +1,18 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.widget.RemoteViews;
import androidx.core.widget.RemoteViewsCompat;
import com.streetwriters.notesnook.datatypes.Reminder;
import java.util.List;
public class ReminderWidgetProvider extends AppWidgetProvider {
static String NewReminder = "com.streetwriters.notesnook.NewReminder";
static String OpenReminderId = "com.streetwriters.notesnook.OpenReminderId";
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
@@ -27,10 +23,20 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
}
private static Bundle getActivityOptionsBundle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ActivityOptions activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return activityOptions.toBundle();
} else
return null;
}
static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId, RemoteViews views) {
Intent listview_intent_template = new Intent(context, MainActivity.class);
listview_intent_template.setAction(Intent.ACTION_VIEW);
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, WidgetUtils.getActivityOptionsBundle());
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, listview_intent_template, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_MUTABLE, getActivityOptionsBundle());
views.setPendingIntentTemplate(R.id.widget_list_view, pendingIntent);
Intent new_reminder_intent = new Intent(context, MainActivity.class);
@@ -38,31 +44,13 @@ public class ReminderWidgetProvider extends AppWidgetProvider {
new_reminder_intent.setAction(Intent.ACTION_VIEW);
new_reminder_intent.putExtra(RCTNNativeModule.IntentType, "NewReminder");
new_reminder_intent.setData(Uri.parse("https://app.notesnook.com/new_reminder"));
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, WidgetUtils.getActivityOptionsBundle());
PendingIntent pendingIntent2 = PendingIntent.getActivity(context, appWidgetId, new_reminder_intent, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
views.setOnClickPendingIntent(R.id.add_button, pendingIntent2);
// The rows travel with the update itself, so there is no bound service to keep in sync and
// nothing to invalidate separately: every update redraws from the current data.
List<Reminder> reminders = WidgetUtils.getWidgetReminders(context);
RemoteViewsCompat.RemoteCollectionItems.Builder items =
new RemoteViewsCompat.RemoteCollectionItems.Builder();
for (Reminder reminder : reminders) {
items.addItem(getItemId(reminder), WidgetUtils.createReminderItem(context, reminder));
}
// Two, because a reminder without a description uses the compact row layout.
items.setViewTypeCount(2);
items.setHasStableIds(true);
RemoteViewsCompat.setRemoteAdapter(context, views, appWidgetId, R.id.widget_list_view, items.build());
Intent list_remote_adapter_intent = new Intent(context, ReminderViewsService.class);
list_remote_adapter_intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
views.setRemoteAdapter(R.id.widget_list_view, list_remote_adapter_intent);
views.setEmptyView(R.id.widget_list_view, R.id.empty_view);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
/**
* Ties a row to its reminder rather than to its position, so rows keep their identity when the
* list shifts around them.
*/
private static long getItemId(Reminder reminder) {
return reminder.getId() == null ? 0 : reminder.getId().hashCode();
}
}
}

View File

@@ -1,28 +0,0 @@
package com.streetwriters.notesnook;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Redraws the widgets when the clock or timezone changes.
*
* Reminder rows are described relative to the current time ("Upcoming"/"Last", "Today"/"Tomorrow"),
* and a reminder that has passed drops off the list entirely. All of that is decided when the
* widget is drawn, so moving the clock leaves the previous drawing in place: a reminder can still
* read "Upcoming" long after its time has gone by.
*
* The app normally redraws the list when a reminder notification is delivered, but that does not
* happen if the reminder never fires, which is exactly the case when the clock jumps past it.
*
* Only TIME_SET and TIMEZONE_CHANGED are handled here: DATE_CHANGED is not exempt from the
* Android 8 limits on manifest-registered implicit broadcasts, so a receiver for it would never
* run. Crossing midnight is instead picked up by the widget's own periodic update.
*/
public class WidgetTimeChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
WidgetUtils.refreshAll(context);
}
}

View File

@@ -1,299 +0,0 @@
package com.streetwriters.notesnook;
import android.app.ActivityOptions;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.util.Log;
import android.widget.RemoteViews;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.streetwriters.notesnook.datatypes.Note;
import com.streetwriters.notesnook.datatypes.Reminder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Shared helpers for the home screen widgets.
*/
public class WidgetUtils {
static final String PREFERENCES = "appPreview";
static final String REMINDERS_KEY = "remindersList";
/**
* Every row is serialized into the widget update itself, which has to fit inside a binder
* transaction, so the list cannot grow without bound. Far more than fits on screen anyway.
*/
private static final int MAX_REMINDERS = 50;
/**
* Redraws every widget that currently exists, and drops stored notes for widgets that no
* longer do.
*
* Everything else keys off what we have stored, which is fine while the app is running but
* leaves widgets showing content that no longer exists once the store is emptied underneath
* them (clearing app data) or a widget is removed while the app is stopped (onDeleted never
* arrives). Starting from the widgets the system knows about, rather than from our own data,
* is what makes this self-correcting.
*
* NoteWidget is left alone deliberately: it is a static button with no stored state, and its
* layout depends on the size it was last given.
*/
static void refreshAll(Context context) {
AppWidgetManager manager = AppWidgetManager.getInstance(context);
int[] noteWidgetIds = manager.getAppWidgetIds(
new ComponentName(context, NotePreviewWidget.class));
removeOrphanedNotes(context, noteWidgetIds);
for (int appWidgetId : noteWidgetIds) {
NotePreviewWidget.updateAppWidget(context, manager, appWidgetId);
}
for (int appWidgetId : manager.getAppWidgetIds(
new ComponentName(context, ReminderWidgetProvider.class))) {
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_reminders);
ReminderWidgetProvider.updateAppWidget(context, manager, appWidgetId, views);
}
}
/**
* Drops stored notes whose widget is gone, so the preferences file cannot grow forever.
*/
private static void removeOrphanedNotes(Context context, int[] liveWidgetIds) {
Set<String> live = new HashSet<>();
for (int appWidgetId : liveWidgetIds) live.add(String.valueOf(appWidgetId));
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
SharedPreferences.Editor edit = preferences.edit();
boolean changed = false;
for (String key : preferences.getAll().keySet()) {
// Leave anything that is not a widget id alone, the reminders list included.
if (parseWidgetId(key) == null || live.contains(key)) continue;
edit.remove(key);
changed = true;
}
if (changed) edit.apply();
}
/**
* The note each note widget is showing, keyed by widget id.
*
* The preferences file mixes two things: one note per widget id, and the reminders list under
* its own key. Only numeric keys are widget notes, so anything else is skipped rather than
* being treated as a note.
*/
static Map<Integer, Note> getWidgetNotes(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
Map<Integer, Note> notes = new LinkedHashMap<>();
for (Map.Entry<String, ?> entry : preferences.getAll().entrySet()) {
Integer widgetId = parseWidgetId(entry.getKey());
if (widgetId == null) continue;
if (!(entry.getValue() instanceof String)) continue;
Note note = parseNote((String) entry.getValue());
if (note == null || note.getId() == null) continue;
notes.put(widgetId, note);
}
return notes;
}
/**
* The widget id a preferences key refers to, or null if the key is not a widget id at all.
*/
private static Integer parseWidgetId(String key) {
try {
return Integer.valueOf(key);
} catch (NumberFormatException e) {
return null;
}
}
static Note parseNote(String data) {
if (data == null || data.isEmpty()) return null;
try {
return new Gson().fromJson(data, Note.class);
} catch (Exception e) {
Log.e("NotePreviewWidget", "Could not read a stored note", e);
return null;
}
}
/**
* The reminders the app last wrote out, minus any that have now dropped out of view. Reading
* and filtering happens here so the provider can push the rows straight into the widget.
*/
static List<Reminder> getWidgetReminders(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
List<Reminder> stored = null;
try {
stored = new Gson().fromJson(preferences.getString(REMINDERS_KEY, "[]"),
new TypeToken<List<Reminder>>() {}.getType());
} catch (Exception e) {
Log.e("Reminders", "Could not read the stored reminders list", e);
}
List<Reminder> active = new ArrayList<>();
if (stored == null) return active;
for (Reminder reminder : stored) {
if (!isVisibleInWidget(reminder)) continue;
if (active.size() >= MAX_REMINDERS) {
Log.w("Reminders", "Widget list truncated to " + MAX_REMINDERS + " reminders");
break;
}
active.add(reminder);
}
return active;
}
/**
* Builds a single row of the reminders list.
*/
static RemoteViews createReminderItem(Context context, Reminder reminder) {
boolean useMiniLayout = reminder.getDescription() == null || reminder.getDescription().isEmpty();
RemoteViews views = new RemoteViews(context.getPackageName(),
useMiniLayout ? R.layout.widget_reminder_layout_small : R.layout.widget_reminder_layout);
views.setTextViewText(R.id.reminder_title, reminder.getTitle());
if (!useMiniLayout) {
views.setTextViewText(R.id.reminder_description, reminder.getDescription());
}
views.setTextViewText(R.id.reminder_time, formatReminderTime(context, reminder));
Intent fillInIntent = new Intent();
fillInIntent.setData(Uri.parse("https://app.notesnook.com/open_reminder?id=" + reminder.getId()));
fillInIntent.putExtra(RCTNNativeModule.IntentType, "OpenReminder");
fillInIntent.putExtra(ReminderWidgetProvider.OpenReminderId, reminder.getId());
views.setOnClickFillInIntent(R.id.reminder_item_btn, fillInIntent);
return views;
}
/**
* Options attached to the PendingIntents our widgets hand to the launcher, opting the creator
* (us) in to background activity starts so a tap on the widget can bring up an activity.
*
* MODE_BACKGROUND_ACTIVITY_START_ALLOWED is deprecated since API 36 and Android 17 extends the
* background activity launch restrictions to IntentSender, so on API 36+ we use the narrower
* MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE instead. That is enough for widgets: the
* sender is the launcher, which is visible whenever the user taps the widget.
*/
static Bundle getActivityOptionsBundle() {
ActivityOptions activityOptions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
activityOptions = ActivityOptions.makeBasic();
activityOptions.setPendingIntentCreatorBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
} else {
return null;
}
return activityOptions.toBundle();
}
/**
* How long a reminder keeps its place in the list after going off, so the user can see that it
* happened rather than watching it vanish. Must match RECENTLY_PASSED_WINDOW in
* services/notifications.ts, which decides what gets written out in the first place.
*/
private static final long RECENTLY_PASSED_WINDOW_MS = TimeUnit.HOURS.toMillis(3);
/**
* Whether a reminder should still be drawn.
*
* We re-check here rather than trusting the stored list because that list is only rewritten
* while the app runs. This is what actually retires a reminder once its grace period is up:
* every redraw re-evaluates it against the current time.
*/
static boolean isVisibleInWidget(Reminder reminder) {
if (reminder == null) return false;
if (reminder.isDisabled()) return false;
long now = System.currentTimeMillis();
if (reminder.getSnoozeUntil() > now) return true;
if (!"once".equals(reminder.getMode())) return true;
long triggerDate = reminder.getTriggerDate() > 0 ? reminder.getTriggerDate() : reminder.getDate();
return triggerDate > now - RECENTLY_PASSED_WINDOW_MS;
}
/**
* Builds the label shown under a reminder. The app sends us the absolute trigger time plus the
* parts that never change ("5:00 PM", "12-05-2026, 5:00 PM"); everything that depends on the
* current time is decided here so it stays right as the widget redraws.
*
* Falls back to the pre-formatted string for lists written by an older version of the app.
*/
static String formatReminderTime(Context context, Reminder reminder) {
long triggerDate = reminder.getTriggerDate();
String timeOfDay = reminder.getFormattedTimeOfDay();
if (triggerDate <= 0 || timeOfDay == null || timeOfDay.isEmpty()) {
return reminder.getFormattedTime();
}
if ("permanent".equals(reminder.getMode())) {
return context.getString(R.string.reminder_ongoing);
}
long now = System.currentTimeMillis();
if (reminder.getSnoozeUntil() > now) {
return context.getString(R.string.reminder_snoozed_until, timeOfDay);
}
String text;
long dayOffset = daysFromToday(triggerDate, now);
if (dayOffset == 0) {
text = context.getString(R.string.reminder_today, timeOfDay);
} else if (dayOffset == 1) {
text = context.getString(R.string.reminder_tomorrow, timeOfDay);
} else if (dayOffset == -1) {
text = context.getString(R.string.reminder_yesterday, timeOfDay);
} else {
text = reminder.getFormattedDateTime();
if (text == null || text.isEmpty()) return reminder.getFormattedTime();
}
return context.getString(
triggerDate <= now ? R.string.reminder_last : R.string.reminder_upcoming, text);
}
/**
* Calendar days between two instants. Compares midnights rather than subtracting the raw
* difference so that "tomorrow" is still tomorrow across a DST change or just before midnight.
*/
private static long daysFromToday(long time, long now) {
long target = startOfDay(time);
long today = startOfDay(now);
return Math.round((target - today) / (double) DateUtils.DAY_IN_MILLIS);
}
private static long startOfDay(long time) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(time);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTimeInMillis();
}
}

View File

@@ -2,14 +2,13 @@ package com.streetwriters.notesnook.datatypes;
import androidx.annotation.Keep;
import java.util.concurrent.TimeUnit;
@Keep
public class Reminder extends BaseItem {
private String title;
private String description;
private String formattedTime;
private String formattedTimeOfDay; // e.g. "5:00 PM"
private String formattedDateTime; // e.g. "12-05-2026, 5:00 PM"
private long triggerDate; // absolute time this reminder next fires
private String priority; // "silent", "vibrate", "urgent"
private long date;
private String mode; // "repeat", "once", "permanent"
@@ -108,27 +107,32 @@ public class Reminder extends BaseItem {
this.formattedTime = formattedTime;
}
public String getFormattedTimeOfDay() {
return formattedTimeOfDay;
}
public String formatTime(long timeInMillis) {
long currentTime = System.currentTimeMillis();
long diff = timeInMillis - currentTime;
public void setFormattedTimeOfDay(String formattedTimeOfDay) {
this.formattedTimeOfDay = formattedTimeOfDay;
}
public String getFormattedDateTime() {
return formattedDateTime;
}
public void setFormattedDateTime(String formattedDateTime) {
this.formattedDateTime = formattedDateTime;
}
public long getTriggerDate() {
return triggerDate;
}
public void setTriggerDate(long triggerDate) {
this.triggerDate = triggerDate;
if (diff < TimeUnit.MINUTES.toMillis(1)) {
return "in " + (diff / 1000) + " seconds";
} else if (diff < TimeUnit.HOURS.toMillis(1)) {
long minutes = TimeUnit.MILLISECONDS.toMinutes(diff);
return "in " + minutes + " minute" + (minutes > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(1)) {
long hours = TimeUnit.MILLISECONDS.toHours(diff);
return "in " + hours + " hour" + (hours > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(2)) {
return "tomorrow";
} else if (diff < TimeUnit.DAYS.toMillis(7)) {
long days = TimeUnit.MILLISECONDS.toDays(diff);
return "in " + days + " day" + (days > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(30)) {
long weeks = TimeUnit.MILLISECONDS.toDays(diff) / 7;
return "in " + weeks + " week" + (weeks > 1 ? "s" : "");
} else if (diff < TimeUnit.DAYS.toMillis(365)) {
long months = TimeUnit.MILLISECONDS.toDays(diff) / 30;
return "in " + months + " month" + (months > 1 ? "s" : "");
} else {
long years = TimeUnit.MILLISECONDS.toDays(diff) / 365;
return "in " + years + " year" + (years > 1 ? "s" : "");
}
}
}

View File

@@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/background"/>
<stroke android:width="0dp" android:color="#B1BCBE" />
<corners android:radius="@dimen/widget_background_radius"/>
<corners android:radius="10dp"/>
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>

View File

@@ -2,7 +2,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
android:background="@android:color/transparent"
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
<LinearLayout
android:layout_width="match_parent"

View File

@@ -2,7 +2,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
android:background="@android:color/transparent"
android:theme="@style/ThemeOverlay.Notesnook.AppWidgetContainer">
<LinearLayout
android:layout_width="match_parent"
@@ -24,7 +25,7 @@
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold"
android:text="@string/widget_note_unconfigured_title" />
android:text="Widget unconfigured" />
<TextView
android:id="@+id/widget_body"
android:layout_width="wrap_content"
@@ -33,7 +34,7 @@
android:layout_marginLeft="8dp"
android:textColor="@color/text"
android:textSize="14sp"
android:text="@string/widget_note_unconfigured_body" />
android:text="Configure this widget to show a note here." />
</LinearLayout>
</RelativeLayout>

View File

@@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Shown in the widget picker only. Mirrors note_widget.xml, but with sample content, since the
real layout has nothing to show until the widget has been configured.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:background="@drawable/layout_bg"
android:elevation="5dp"
android:orientation="vertical"
android:paddingHorizontal="10dp"
android:paddingVertical="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:text="@string/widget_preview_note_title"
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="8dp"
android:text="@string/widget_preview_note_body"
android:textColor="@color/text"
android:textSize="14sp" />
</LinearLayout>
</RelativeLayout>

View File

@@ -0,0 +1,8 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text="No upcoming reminders"
android:textSize="16sp"
android:textColor="@android:color/darker_gray"
android:gravity="center" />

View File

@@ -12,23 +12,22 @@
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:layout_gravity="center"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingTop="12dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginTop="2dp"
android:textSize="15sp"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="@color/text"
android:text="Reminders"/>
android:text="Upcoming Reminders"/>
<ImageButton
android:layout_width="25dp"
android:layout_width="35dp"
android:id="@+id/add_button"
android:layout_height="25dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:background="@drawable/ic_newnote" />
</RelativeLayout>
@@ -58,8 +57,7 @@
android:layout_height="match_parent"
android:textAlignment="center"
android:gravity="center"
android:textColor="@color/text"
android:text="@string/widget_reminders_empty"/>
android:text="Tap on + to add reminder"/>
</LinearLayout>

View File

@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Shown in the widget picker only. Mirrors widget_reminders.xml, but with the list replaced by
sample rows, since an adapter-backed list renders empty in the picker.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/layout_bg"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingHorizontal="12dp"
android:paddingTop="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_marginTop="2dp"
android:text="Reminders"
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold" />
<ImageButton
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_alignParentRight="true"
android:background="@drawable/ic_newnote"
android:contentDescription="@string/add_widget" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="8dp"
android:background="@color/border" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="12dp"
android:paddingBottom="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/widget_preview_reminder_title"
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/widget_preview_reminder_time"
android:textColor="@color/text"
android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/widget_preview_reminder_title_alt"
android:textColor="@color/text"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/widget_preview_reminder_time_alt"
android:textColor="@color/text"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="light_blue_50">#FFE1F5FE</color>
<color name="light_blue_200">#FF81D4FA</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="bootsplash_background">#1f1f1f</color>
<color name="background">#1D1D1D</color>
<color name="border">#2E2E2E</color>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Android 12 onwards the launcher tells us what radius widgets should use, so ours line up
with every other widget on the home screen instead of being a fixed 10dp. -->
<dimen name="widget_background_radius">@android:dimen/system_app_widget_background_radius</dimen>
</resources>

View File

@@ -0,0 +1,6 @@
<resources>
<declare-styleable name="AppWidgetAttrs">
<attr name="appWidgetBackgroundColor" format="color" />
<attr name="appWidgetTextColor" format="color" />
</declare-styleable>
</resources>

View File

@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="light_blue_50">#FFE1F5FE</color>
<color name="light_blue_200">#FF81D4FA</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="bootsplash_background">#FFFFFF</color>
<color name="background">#DCEDEDED</color>
<color name="border">#BFBFBF</color>

View File

@@ -7,8 +7,4 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
-->
<dimen name="widget_margin">0dp</dimen>
<!-- Overridden in values-v31 with the platform's own widget radius, so our widgets match the
rest of the home screen. This is the fallback for older versions. -->
<dimen name="widget_background_radius">10dp</dimen>
</resources>

View File

@@ -4,28 +4,9 @@
<string name="appwidget_text">EXAMPLE</string>
<string name="add_widget">Add widget</string>
<string name="take_a_quick_note">Take a quick note.</string>
<string name="reminders">Quick overview of reminders</string>
<string name="reminders">Quick overview of upcoming reminders</string>
<string name="reminders_title">Reminders</string>
<string name="note">Note</string>
<string name="note_description">Add a note to home screen</string>
<string name="quick_note">Quick note</string>
<string name="widget_reminders_empty">Tap + to add a reminder</string>
<!-- Sample content, only ever shown in the widget picker's preview. -->
<string name="widget_preview_note_title">Meeting notes</string>
<string name="widget_preview_note_body">Discuss the roadmap and agree on timelines.</string>
<string name="widget_preview_reminder_title">Take a walk</string>
<string name="widget_preview_reminder_time">Upcoming: Today, 5:00 PM</string>
<string name="widget_preview_reminder_title_alt">Call the dentist</string>
<string name="widget_preview_reminder_time_alt">Upcoming: Tomorrow, 9:00 AM</string>
<string name="widget_note_unconfigured_title">Tap to choose a note</string>
<string name="widget_note_unconfigured_body">Pick the note you want shown here.</string>
<string name="reminder_ongoing">Ongoing</string>
<string name="reminder_snoozed_until">Snoozed until %1$s</string>
<string name="reminder_today">Today, %1$s</string>
<string name="reminder_tomorrow">Tomorrow, %1$s</string>
<string name="reminder_yesterday">Yesterday, %1$s</string>
<string name="reminder_upcoming">Upcoming: %1$s</string>
<string name="reminder_last">Last: %1$s</string>
</resources>

View File

@@ -0,0 +1,7 @@
<resources>
<style name="ThemeOverlay.Notesnook.AppWidgetContainer" parent="">
<item name="appWidgetBackgroundColor">@color/light_blue_600</item>
<item name="appWidgetTextColor">@color/light_blue_50</item>
</style>
</resources>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
Keep this in sync with res/xml/note_widget_info.xml. It exists only to add not_keyguard: from
Android 16 QPR1 widgets are lock screen eligible by default, and this one renders the note's
title and preview text, which should not be readable on a locked device.
-->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/note_widget"
android:initialLayout="@layout/note_widget"
android:configure="com.streetwriters.notesnook.NotePreviewConfigureActivity"
android:widgetFeatures="reconfigurable"
android:minResizeWidth="100dp"
android:minResizeHeight="50dp"
android:minWidth="400dp"
android:description="@string/note_description"
android:minHeight="50dp"
android:targetCellWidth="5"
android:targetCellHeight="1"
android:previewImage="@drawable/note_widget_preview"
android:previewLayout="@layout/note_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen|not_keyguard"/>

View File

@@ -1,15 +0,0 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_reminders"
android:minWidth="250dp"
android:minHeight="110dp"
android:minResizeWidth="180dp"
android:minResizeHeight="110dp"
android:description="@string/reminders"
android:targetCellWidth="5"
android:targetCellHeight="2"
android:resizeMode="horizontal|vertical"
android:previewImage="@drawable/reminder_preview"
android:previewLayout="@layout/widget_reminders_preview"
android:updatePeriodMillis="1800000"
android:widgetCategory="home_screen|not_keyguard"
/>

View File

@@ -10,7 +10,6 @@
android:targetCellWidth="5"
android:targetCellHeight="1"
android:previewImage="@drawable/widget_preview"
android:previewLayout="@layout/new_note_widget"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"/>

View File

@@ -12,7 +12,6 @@
android:targetCellWidth="5"
android:targetCellHeight="1"
android:previewImage="@drawable/note_widget_preview"
android:previewLayout="@layout/note_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"/>

View File

@@ -1,15 +1,14 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_reminders"
android:minWidth="250dp"
android:minHeight="110dp"
android:minResizeWidth="180dp"
android:minResizeHeight="110dp"
android:minWidth="400dp"
android:minHeight="100dp"
android:minResizeWidth="400dp"
android:minResizeHeight="50dp"
android:description="@string/reminders"
android:targetCellWidth="5"
android:targetCellHeight="2"
android:resizeMode="horizontal|vertical"
android:previewImage="@drawable/reminder_preview"
android:previewLayout="@layout/widget_reminders_preview"
android:updatePeriodMillis="1800000"
android:updatePeriodMillis="1024"
android:widgetCategory="home_screen"
/>

View File

@@ -13,7 +13,6 @@ buildscript {
androidXCore = "1.7.0"
androidXBrowser = "1.0.0"
ndkVersion = "27.1.12297006"
playBillingSdkVersion = "8.0.0"
}
repositories {
@@ -42,12 +41,3 @@ allprojects {
maven { url 'https://www.jitpack.io' }
}
}
// Pin the ndkVersion for all subprojects
subprojects { subproject ->
["com.android.application", "com.android.library"].each { pluginId ->
subproject.plugins.withId(pluginId) {
subproject.android.ndkVersion = rootProject.ext.ndkVersion
}
}
}

View File

@@ -1,3 +1,4 @@
- Add option to clear note version history
- Minor bug fixes and improvements
Thank you for using Notesnook!

View File

@@ -23,7 +23,7 @@ import {
THEME_COMPATIBILITY_VERSION,
useThemeEngineStore
} from "@notesnook/theme";
import React, { PropsWithChildren, useEffect, useState } from "react";
import React, { PropsWithChildren, useEffect } from "react";
import { Appearance, I18nManager, Linking, StatusBar } from "react-native";
import "react-native-gesture-handler";
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -44,37 +44,26 @@ 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";
import {
initShortcutListener,
launchNewNoteTab,
registerAppShortcuts
} from "./hooks/use-shortcut-manager";
import Shortcuts from "react-native-actions-shortcuts";
I18nManager.allowRTL(false);
I18nManager.forceRTL(false);
I18nManager.swapLeftAndRightInRTL(false);
const { appLockEnabled, appLockMode } = SettingsService.get();
if (appLockEnabled || appLockMode !== "none") {
useUserStore.getState().lockApp(true);
}
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;
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
useEffect(() => {
if (introCompleted) {
registerAppShortcuts();
}
}, [introCompleted]);
useEffect(() => {
RNBootSplash.hide({ fade: true });
SettingsService.onFirstLaunch();
changeSystemBarColors();
SettingsService.setPrivacyScreen(
@@ -187,41 +176,4 @@ export const withTheme = (
};
};
export const withStartupBoundry = (
Element: (props: PropsWithChildren) => JSX.Element
) => {
return function AppWithStartupBoundary(props: PropsWithChildren) {
const [ready, setReady] = useState(false);
useEffect(() => {
async function init() {
try {
const [url, shortcut] = await Promise.all([
Linking.getInitialURL(),
Shortcuts.getInitialShortcut()
]);
console.log(url, shortcut);
if (shortcut?.type === "notesnook.action.newnote") {
launchNewNoteTab();
}
useSettingStore.setState({
initialUrl: url,
pendingShortcut: shortcut ?? null
});
initShortcutListener();
} finally {
setReady(true);
}
}
init();
}, []);
if (!ready) return null;
return <Element {...props} />;
};
};
export default withStartupBoundry(withTheme(withErrorBoundry(App, "App")));
export default withTheme(withErrorBoundry(App, "App"));

View File

@@ -24,8 +24,6 @@ import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth } from "./common";
import { AuthParams } from "../../stores/use-navigation-store";
import { strings } from "@notesnook/intl";
import { presentDialog } from "../dialog/functions";
export const AuthHeader = (props: { welcome?: boolean }) => {
const { colors } = useThemeColors();
const route = useRoute();
@@ -58,17 +56,18 @@ export const AuthHeader = (props: { welcome?: boolean }) => {
{!props.welcome ? null : (
<Button
title={strings.skipAndGoToApp()}
title="Skip"
onPress={() => {
presentDialog({
title: strings.offlineMode(),
paragraph: strings.offlineModeDesc(),
positiveText: strings.understand(),
positivePress: hideAuth as any
});
hideAuth();
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: 6
}}

View File

@@ -36,7 +36,6 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
interface FloatingButtonProps {
onPress: () => void;
@@ -48,7 +47,6 @@ interface FloatingButtonProps {
position?: "left" | "right";
size?: "small" | "large";
style?: ViewStyle;
hideOnKeyboard?: boolean;
}
const FloatingButton = ({
@@ -59,28 +57,26 @@ const FloatingButton = ({
testID,
position = "right",
size = "large",
style,
hideOnKeyboard = true
style
}: FloatingButtonProps) => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const selectionMode = useSelectionStore((state) => state.selectionMode);
const translate = useSharedValue(0);
const keyboardHeight = useSharedValue(0);
const route = useRoute();
const insets = useGlobalSafeAreaInsets();
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{
translateX: hideOnKeyboard ? translate.value : 0
},
{
translateY: hideOnKeyboard ? translate.value : 0
}
],
bottom: hideOnKeyboard ? 15 : keyboardHeight.value + 15
}));
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translate.value
},
{
translateY: translate.value
}
]
};
});
const animate = useCallback(
(toValue: number) => {
@@ -101,22 +97,12 @@ const FloatingButton = ({
editorState().keyboardState = false;
if (deviceMode !== "mobile") return;
animate(0);
keyboardHeight.value = withTiming(0);
};
const onKeyboardShow = (e: any) => {
const onKeyboardShow = () => {
editorState().keyboardState = true;
if (deviceMode !== "mobile") return;
if (hideOnKeyboard) {
animate(150);
} else {
keyboardHeight.value = withTiming(
e.endCoordinates.height - insets.bottom,
{
duration: 250
}
);
}
animate(150);
};
const sub = [
@@ -124,9 +110,9 @@ const FloatingButton = ({
Keyboard.addListener("keyboardDidHide", onKeyboardHide)
];
return () => {
sub.forEach((sub) => sub.remove());
sub.forEach((sub) => sub?.remove?.());
};
}, [deviceMode, animate, hideOnKeyboard, keyboardHeight, insets.bottom]);
}, [deviceMode, animate]);
return deviceMode !== "mobile" && !alwaysVisible ? null : (
<Animated.View
@@ -175,10 +161,10 @@ const FloatingButton = ({
icon
? icon
: route.name === "Notebooks"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
? "notebook-plus"
: route.name === "Trash"
? "delete"
: "plus"
}
color={color || colors.primary.accent}
size={size === "small" ? AppFontSize.xl : AppFontSize.xxxl}

View File

@@ -143,6 +143,9 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
}, [hide, show]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
await dialogInfo.onClose();
}
hide();
};

View File

@@ -51,10 +51,9 @@ interface TabProps extends ViewProps {
onScroll: (offset: number) => void;
enabled: boolean;
onDrawerStateChange: (state: boolean) => void;
initialPage?: FluidTabPage;
}
export type FluidTabPage = "home" | "editor";
type FluidTabPage = "home" | "editor";
export interface TabsRef {
goToPage: (page: FluidTabPage, animated?: boolean) => void;
@@ -78,21 +77,15 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
onChangeTab,
onScroll,
enabled,
onDrawerStateChange,
initialPage
onDrawerStateChange
}: TabProps,
ref
) {
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const editorStartPosition = widths.sidebar + widths.list;
const translateX = useSharedValue(
initialPage === "editor" ? editorStartPosition : widths ? widths.sidebar : 0
);
const translateX = useSharedValue(widths ? widths.sidebar : 0);
const startX = useSharedValue(0);
const currentTab = useSharedValue(
initialPage === "editor" && deviceMode !== "tablet" ? 2 : 1
);
const currentTab = useSharedValue(1);
const previousTab = useSharedValue(1);
const isDrawerOpen = useSharedValue(false);
const gestureStartValue = useSharedValue({

View File

@@ -58,9 +58,9 @@ export const openNote = async (
}
if (isTrash) {
const content = note.contentId
? await db.content.get(note.contentId)
: undefined;
if (!note.contentId) return;
const content = await db.content.get(note.contentId as string);
presentSheet({
component: <NotePreview note={item} content={content} />
});

View File

@@ -249,7 +249,7 @@ export default function NoteHistory({
<Text
onPress={() => {
openLinkInBrowser(
"https://notesnook.com/help/note-version-history"
"https://help.notesnook.com/note-version-history"
);
}}
style={{

View File

@@ -198,7 +198,7 @@ export default function NotePreview({
<View
style={{
width: "100%",
flex: 1,
height: 100,
justifyContent: "center",
alignItems: "center"
}}

View File

@@ -50,7 +50,7 @@ export const Synced = ({ item, close }) => {
close();
await sleep(300);
await openLinkInBrowser(
"https://notesnook.com/help/how-is-my-data-encrypted",
"https://help.notesnook.com/how-is-my-data-encrypted",
colors
);
} catch (e) {

View File

@@ -509,7 +509,7 @@ const PublishNoteSheet = ({
onPress={async () => {
try {
await openLinkInBrowser(
"https://notesnook.com/help/publish-notes-with-monographs"
"https://help.notesnook.com/publish-notes-with-monographs"
);
} catch (e) {
console.error(e);

View File

@@ -230,7 +230,7 @@ class RecoveryKeySheet extends React.Component {
>
<Paragraph
color={colors.primary.paragraph}
size={AppFontSize.md}
size={AppFontSize.sm}
numberOfLines={2}
selectable
style={{
@@ -238,9 +238,7 @@ class RecoveryKeySheet extends React.Component {
maxWidth: "100%",
paddingRight: 10,
textAlign: "center",
textDecorationLine: "underline",
letterSpacing: 0.5,
fontFamily: "monospace"
textDecorationLine: "underline"
}}
>
{this.state.key}

View File

@@ -33,6 +33,7 @@ import {
setGroupOptionsById
} from "../../../hooks/use-group-options";
import { eSendEvent } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { RouteName } from "../../../stores/use-navigation-store";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useTagStore } from "../../../stores/use-tag-store";
@@ -45,7 +46,6 @@ import { Button } from "../../ui/button";
import { Pressable } from "../../ui/pressable";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import Navigation from "../../../services/navigation";
const Sort = ({
dataType,
screen,
@@ -87,7 +87,8 @@ const Sort = ({
};
const updateGroupOptions = async (_groupOptions: GroupOptions) => {
await setGroupOptionsById(groupType, _groupOptions, groupId, type);
console.log(groupId, type);
setGroupOptionsById(groupType, _groupOptions, groupId, type);
setGroupOptions(_groupOptions);
setTimeout(() => {
if (screen) Navigation.queueRoutesForUpdate(screen);

View File

@@ -30,7 +30,6 @@ import { Pressable } from "../ui/pressable";
import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import { useSideBarDraggingStore } from "./dragging-store";
import SyncStatusButton from "./sync-status-button";
const SettingsIcon = () => {
const { colors } = useThemeColors();
@@ -124,7 +123,7 @@ export const SideMenuHeader = (props: { rightButtons?: IconButtonProps[] }) => {
size={AppFontSize.lg}
/>
))}
<SyncStatusButton />
<SettingsIcon />
</View>
</View>

View File

@@ -1,161 +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 { useTimeAgo } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useNetInfo } from "@react-native-community/netinfo";
import React from "react";
import { View } from "react-native";
import Animated, {
Easing,
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming
} from "react-native-reanimated";
import Sync from "../../services/sync";
import { SyncStatus, useUserStore } from "../../stores/use-user-store";
import { AppFontSize } from "../../utils/size";
import NativeTooltip from "../../utils/tooltip";
import AppIcon from "../ui/AppIcon";
import { IconButton } from "../ui/icon-button";
const SyncStatusButton = () => {
const { colors } = useThemeColors();
const [user, syncing, lastSyncStatus, lastSynced, isLoggingOut] =
useUserStore((state) => [
state.user,
state.syncing,
state.lastSyncStatus,
state.lastSynced,
state.isLoggingOut
]);
const { isInternetReachable } = useNetInfo();
const isOffline = !isInternetReachable;
const hasSyncedBefore = lastSynced && lastSynced !== "Never";
const isFailed = lastSyncStatus === SyncStatus.Failed;
const isSynced = lastSyncStatus === SyncStatus.Passed;
const lastSyncedTimeAgo = useTimeAgo(lastSynced, {
interval: 5000,
live: true
});
const getIconColor = (): string => {
if (syncing) return colors.primary.accent;
if (isSynced) return colors.primary.accent;
return colors.secondary.icon;
};
const rotation = useSharedValue(0);
React.useEffect(() => {
if (syncing && !isOffline) {
rotation.value = 0;
rotation.value = withRepeat(
withTiming(360, { duration: 1000, easing: Easing.linear }),
-1,
false
);
} else {
cancelAnimation(rotation);
rotation.value = withTiming(360, {
duration: 1000 * (1 - (rotation.value % 360) / 360),
easing: Easing.linear
});
}
}, [syncing, rotation, isOffline]);
const rotationStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }]
}));
const tooltipText = React.useMemo(() => {
const offlineSuffix = isOffline ? ` (${strings.offline()})` : "";
if (syncing) return strings.syncing();
if (!hasSyncedBefore) return `${strings.never()}${offlineSuffix}`;
if (isFailed) return `${strings.syncFailed()}${offlineSuffix}`;
return `${strings.synced()}${lastSyncedTimeAgo}${offlineSuffix ? `${offlineSuffix}` : ""}`;
}, [isOffline, syncing, hasSyncedBefore, isFailed, lastSyncedTimeAgo]);
const onPress = () => {
if (syncing) return;
Sync.run();
};
if (!user || isLoggingOut) return null;
return (
<View
style={{
justifyContent: "center",
alignItems: "center",
position: "relative"
}}
>
{syncing ? (
<View
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border,
justifyContent: "center",
alignItems: "center"
}}
>
<Animated.View style={rotationStyle}>
<AppIcon name="sync" size={AppFontSize.lg} color={getIconColor()} />
</Animated.View>
</View>
) : (
<IconButton
name="sync"
onPress={onPress}
tooltipText={tooltipText}
tooltipPosition={NativeTooltip.POSITIONS.BOTTOM}
size={AppFontSize.lg}
color={getIconColor()}
style={{
width: 28,
height: 28,
borderRadius: 6,
borderWidth: 1,
borderColor: colors.primary.border
}}
/>
)}
{!syncing && (isFailed || isOffline) ? (
<AppIcon
name="information"
color={isOffline ? colors.static.yellow : colors.error.icon}
size={AppFontSize.xxs}
style={{ position: "absolute", bottom: -3, right: -3 }}
/>
) : null}
</View>
);
};
export default SyncStatusButton;

View File

@@ -27,7 +27,7 @@ import { useUserStore } from "../../../stores/use-user-store";
import { getContainerBorder } from "../../../utils/colors";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { Toast } from "../../toast";
import { useReduceMotion } from "../../../hooks/use-reduce-motion";
/**
*
* @param {any} param0
@@ -53,9 +53,6 @@ const SheetWrapper = ({
const sheetKeyboardHandler = useSettingStore(
(state) => state.sheetKeyboardHandler
);
const isReduceMotionEnabled = useReduceMotion();
const isAnimated = !isReduceMotionEnabled;
const largeTablet = deviceMode === "tablet";
const smallTablet = deviceMode === "smallTablet";
const dimensions = useSettingStore((state) => state.dimensions);
@@ -128,7 +125,6 @@ const SheetWrapper = ({
<ScopedThemeProvider value="sheet">
<ActionSheet
ref={fwdRef || localRef}
animated={isAnimated}
testIDs={{
backdrop: "sheet-backdrop"
}}

View File

@@ -81,7 +81,6 @@ import {
setUpdateAvailableMessage
} from "../services/message";
import Navigation from "../services/navigation";
import { NotePreviewWidget } from "../services/note-preview-widget";
import Notifications from "../services/notifications";
import PremiumService from "../services/premium";
import SettingsService from "../services/settings";
@@ -346,7 +345,6 @@ const onLogout = async (reason: string) => {
SettingsService.resetSettings();
useUserStore.getState().setUser(null);
useUserStore.getState().setSyncing(false);
useUserStore.getState().setIsLoggingOut(false);
eSendEvent(eAfterSync);
};
@@ -576,11 +574,6 @@ export const useAppEvents = () => {
useEffect(() => {
if (isAppLoading) return;
// Widgets outlive the app process, so they can be left showing content the app no longer has
// (most obviously after the user clears app data). Nothing can run at that moment, so the
// first launch afterwards is the earliest chance to put them right.
NotePreviewWidget.updateNotes();
let subscriptions: EventManagerSubscription[] = [];
const eventManager = db.eventManager;
subscriptions = [

View File

@@ -54,13 +54,15 @@ export function useGroupOptions(
const [groupOptions, setGroupOptions] = useState(
getGroupOptions(groupingKey, id, type)
);
console.log(groupingKey, id, type, groupOptions, "options");
const groupOptionsRef = useRef(groupOptions);
groupOptionsRef.current = groupOptions;
useEffect(() => {
const onUpdate = (_groupingKey: string, _id?: string, _type?: string) => {
if (_groupingKey !== groupingKey || _type !== type) return;
if (_id && _type && _id !== id) return;
if (_groupingKey !== groupingKey) return;
if (_id && _type && _id !== id && _type !== type) return;
const options = getGroupOptions(groupingKey, id, type);
if (!options) return;
if (
@@ -68,7 +70,9 @@ export function useGroupOptions(
groupOptionsRef.current?.sortBy !== options.sortBy ||
groupOptionsRef.current?.sortDirection !== options?.sortDirection
) {
console.log("onUpdate", _id, _type);
setGroupOptions({ ...options });
Navigation.queueRoutesForUpdate();
}
};

View File

@@ -1,50 +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 { useEffect, useState } from "react";
import { AccessibilityInfo } from "react-native";
export function useReduceMotion(): boolean {
const [isReduceMotionEnabled, setIsReduceMotionEnabled] =
useState<boolean>(false);
useEffect(() => {
AccessibilityInfo.isReduceMotionEnabled()
.then(setIsReduceMotionEnabled)
.catch(() => {});
const subscription = AccessibilityInfo.addEventListener(
"reduceMotionChanged",
setIsReduceMotionEnabled
);
return () => {
if (subscription?.remove) {
subscription.remove();
} else if ((AccessibilityInfo as any).removeEventListener) {
(AccessibilityInfo as any).removeEventListener(
"reduceMotionChanged",
setIsReduceMotionEnabled
);
}
};
}, []);
return isReduceMotionEnabled;
}

View File

@@ -17,67 +17,54 @@ 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 Shortcuts, { ShortcutItem } from "react-native-actions-shortcuts";
import { NativeEventEmitter, NativeModule, Platform } from "react-native";
import { useEffect } from "react";
import { NativeEventEmitter, NativeModule } from "react-native";
import { useRef } from "react";
import { Platform } from "react-native";
import deviceInfoModule from "react-native-device-info";
import { strings } from "@notesnook/intl";
import { useSettingStore } from "../stores/use-setting-store";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
const ShortcutsEmitter = new NativeEventEmitter(
Shortcuts as unknown as NativeModule
);
export function isShortcutsSupported() {
function isSupported() {
return Platform.OS !== "android" || deviceInfoModule.getApiLevelSync() > 25;
}
const defaultShortcuts: ShortcutItem[] = [
{
type: "notesnook.action.newnote",
title: strings.createNewNote(),
shortTitle: strings.newNote(),
iconName: Platform.OS === "android" ? "ic_newnote" : "plus"
},
{
type: "notesnook.action.newreminder",
title: strings.setReminder(),
shortTitle: strings.newReminder(),
iconName: Platform.OS === "android" ? "ic_newnote" : "plus"
}
];
export const useShortcutManager = ({
onShortcutPressed,
shortcuts = defaultShortcuts
}: {
onShortcutPressed: (shortcut: ShortcutItem | null) => void;
shortcuts?: ShortcutItem[];
}) => {
const initialShortcutRecieved = useRef(false);
export function registerAppShortcuts(
shortcuts: ShortcutItem[] = defaultShortcuts
) {
if (!isShortcutsSupported()) return;
Shortcuts.setShortcuts(shortcuts);
}
useEffect(() => {
if (!isSupported()) return;
Shortcuts.setShortcuts(shortcuts);
}, [shortcuts]);
let listenerInitialized = false;
export function initShortcutListener() {
if (!isShortcutsSupported() || listenerInitialized) return;
listenerInitialized = true;
ShortcutsEmitter.addListener(
"onShortcutItemPressed",
(shortcut: ShortcutItem) => {
console.time("shortcut");
useSettingStore.setState({ pendingShortcut: shortcut });
}
);
}
export function launchNewNoteTab() {
let tabId;
const currentTab = useTabStore
.getState()
.getTab(useTabStore.getState().currentTab as string);
if (useTabStore.getState().tabs.length === 0 || currentTab?.pinned) {
tabId = useTabStore.getState().newTab();
} else {
tabId = useTabStore.getState().currentTab;
if (useTabStore.getState().getTab(tabId)?.session?.noteId) {
useTabStore.getState().newTabSession(tabId, {});
}
}
}
useEffect(() => {
if (!isSupported()) return;
Shortcuts.getInitialShortcut().then((shortcut) => {
if (initialShortcutRecieved.current || !shortcut) return;
onShortcutPressed(shortcut);
initialShortcutRecieved.current = true;
});
const subscription = ShortcutsEmitter.addListener(
"onShortcutItemPressed",
onShortcutPressed
);
return () => {
subscription?.remove();
};
}, [onShortcutPressed]);
};

View File

@@ -41,9 +41,10 @@ import Animated, {
} from "react-native-reanimated";
import { notesnook } from "../../e2e/test.ids";
import { db } from "../common/database";
import { FluidPanels, FluidTabPage } from "../components/fluid-panels";
import { FluidPanels } from "../components/fluid-panels";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import useGlobalSafeAreaInsets from "../hooks/use-global-safe-area-insets";
import { useShortcutManager } from "../hooks/use-shortcut-manager";
import { hideAllTooltips } from "../hooks/use-tooltip";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import { editorController, editorState } from "../screens/editor/tiptap/utils";
@@ -58,6 +59,7 @@ import {
eCloseFullscreenEditor,
eOnEnterEditor,
eOnExitEditor,
eOnLoadNote,
eOpenFullscreenEditor,
eUnlockNote
} from "../utils/events";
@@ -65,7 +67,6 @@ import { valueLimiter } from "../utils/functions";
import { fluidTabsRef } from "../utils/global-refs";
import { AppNavigationStack } from "./navigation-stack";
import type { PaneWidths } from "../screens/editor/wrapper";
import { NavigationProps } from "../services/navigation";
const MOBILE_SIDEBAR_SIZE = 0.85;
@@ -73,7 +74,7 @@ let SideMenu: any = null;
let EditorWrapper: any = null;
export const FluidPanelsView = React.memo(
({ route }: NavigationProps<"FluidPanelsView">) => {
() => {
const { colors } = useThemeColors();
const deviceMode = useSettingStore((state) => state.deviceMode);
const setFullscreen = useSettingStore((state) => state.setFullscreen);
@@ -101,15 +102,6 @@ export const FluidPanelsView = React.memo(
setOrientation(o);
}
});
React.useEffect(() => {
const shortcut = useSettingStore.getState().pendingShortcut;
if (shortcut?.type === "notesnook.action.newnote") {
useSettingStore.setState({
pendingShortcut: null
});
}
}, []);
useEffect(() => {
if (!appLoading) {
@@ -119,6 +111,29 @@ export const FluidPanelsView = React.memo(
}
}, [appLoading]);
useShortcutManager({
onShortcutPressed: async (item) => {
if (!item) return;
if (item?.type === "notesnook.action.newnote") {
if (!fluidTabsRef.current) {
setTimeout(() => {
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
fluidTabsRef.current?.goToPage("editor", false);
}, 1000);
return;
}
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
setTimeout(
() => fluidTabsRef.current?.goToPage("editor", false),
300
);
}
}
});
const showFullScreenEditor = useCallback(() => {
setFullscreen(true);
if (deviceMode === "smallTablet") {
@@ -341,7 +356,6 @@ export const FluidPanelsView = React.memo(
dimensions={dimensions}
widths={PANE_WIDTHS[deviceMode as keyof typeof PANE_WIDTHS]}
enabled={deviceMode !== "tablet" && !fullscreen}
initialPage={route.params?.initialPage}
onScroll={onScroll}
onChangeTab={onChangeTab}
onDrawerStateChange={(state) => {

View File

@@ -27,17 +27,10 @@ import useNavigationStore, {
} from "../stores/use-navigation-store";
import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { fluidTabsRef, rootNavigatorRef } from "../utils/global-refs";
import { rootNavigatorRef } from "../utils/global-refs";
import Navigation from "../services/navigation";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import { isFeatureAvailable } from "@notesnook/common";
import { isInternalLink, parseInternalLink } from "@notesnook/core";
import { eSendEvent } from "../services/event-manager";
import { editorState } from "../screens/editor/tiptap/utils";
import { eOnLoadNote } from "../utils/events";
import { strings } from "@notesnook/intl";
import PaywallSheet from "../components/sheets/paywall";
import { presentDialog } from "../components/dialog/functions";
import { launchNewNoteTab } from "../hooks/use-shortcut-manager";
const RootStack = createNativeStackNavigator();
const AppStack = createNativeStackNavigator();
@@ -307,15 +300,8 @@ export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const initialShortcut = React.useRef(
useSettingStore.getState().pendingShortcut
).current;
const reminderFeature = useIsFeatureAvailable("activeReminders");
const clearSelection = useSelectionStore((state) => state.clearSelection);
const resetTimer = React.useRef<NodeJS.Timeout>(undefined);
const onStateChange = React.useCallback(
(state: any) => {
if (useSelectionStore.getState().selectionMode) {
@@ -330,66 +316,13 @@ export const RootNavigation = () => {
[clearSelection]
);
React.useEffect(() => {
const unsubscribe = useSettingStore.subscribe((state, prevState) => {
const pendingShortcut = state.pendingShortcut;
if (pendingShortcut === prevState.pendingShortcut || !pendingShortcut) {
return;
}
if (pendingShortcut.type === "notesnook.action.newreminder") {
if (reminderFeature === undefined) return;
if (!reminderFeature.isAllowed) {
presentDialog({
title: strings.upgrade(),
paragraph: reminderFeature.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
PaywallSheet.present(reminderFeature);
}
});
useSettingStore.setState({
pendingShortcut: null
});
return;
}
rootNavigatorRef.current?.navigate("AddReminder" as any);
} else if (pendingShortcut.type === "notesnook.action.newnote") {
if (fluidTabsRef.current) {
rootNavigatorRef.current?.navigate("FluidPanelsView" as any);
eSendEvent(eOnLoadNote, { newNote: true });
editorState().movedAway = false;
fluidTabsRef.current.goToPage("editor", true);
} else {
launchNewNoteTab();
rootNavigatorRef.current?.navigate("FluidPanelsView" as any, {
initialPage: "editor"
});
}
}
});
return unsubscribe;
}, [reminderFeature]);
const initialRouteName = !introCompleted
? "Welcome"
: initialShortcut?.type === "notesnook.action.newreminder"
? "AddReminder"
: "FluidPanelsView";
return (
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
<RootStack.Navigator
screenOptions={{
headerShown: false
}}
initialRouteName={initialRouteName}
initialRouteName={introCompleted ? "FluidPanelsView" : "Welcome"}
>
<RootStack.Screen
name="Welcome"
@@ -414,12 +347,6 @@ export const RootNavigation = () => {
require("../navigation/fluid-panels-view").default;
return FluidPanelsView;
}}
initialParams={{
initialPage:
initialShortcut?.type === "notesnook.action.newnote"
? "editor"
: undefined
}}
/>
<RootStack.Screen

View File

@@ -20,9 +20,8 @@ import { Note, Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useRef, useState } from "react";
import {
BackHandler,
KeyboardAvoidingView,
Platform,
ScrollView,
@@ -64,7 +63,6 @@ import FormInput, {
validators
} from "../../components/ui/input/form-input";
import AppIcon from "../../components/ui/AppIcon";
import { presentDialog } from "../../components/dialog/functions";
const ReminderModes =
Platform.OS === "ios"
@@ -96,7 +94,7 @@ const ReminderNotificationModes = {
};
export default function AddReminder(props: NavigationProps<"AddReminder">) {
const { reminder, reference } = props.route.params ?? {};
const { reminder, reference } = props.route.params;
useNavigationFocus(props.navigation, {
focusOnInit: true,
onFocus: () => {
@@ -108,23 +106,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
return false;
}
});
const handleBackNavigation = useCallback(() => {
const routes = props.navigation.getState()?.routes;
if (routes && routes.length <= 1) {
props.navigation.navigate("FluidPanelsView" as any);
return true;
}
Navigation.goBack();
return true;
}, [props.navigation]);
useEffect(() => {
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
return handleBackNavigation();
});
return () => sub.remove();
}, [handleBackNavigation]);
const { colors, isDark } = useThemeColors();
const weekFormat = useSettingStore((state) => state.weekFormat);
const [reminderMode, setReminderMode] = useState<Reminder["mode"]>(
@@ -146,7 +127,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference ? (reference as Note) : null;
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
const activeReminderFeature = useIsFeatureAvailable("activeReminders");
const formRef = useRef(
createFormRef({
title: reminder?.title || referencedItem?.title || "",
@@ -173,31 +153,6 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
);
const [dateError, setDateError] = useState<string>();
const [selectDayError, setSelectDayError] = useState<string>();
React.useEffect(() => {
const shortcut = useSettingStore.getState().pendingShortcut;
if (shortcut?.type === "notesnook.action.newreminder") {
useSettingStore.setState({
pendingShortcut: null
});
}
}, []);
useEffect(() => {
if (activeReminderFeature === undefined) return;
if (!activeReminderFeature.isAllowed) {
presentDialog({
title: strings.upgrade(),
paragraph: activeReminderFeature.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
PaywallSheet.present(activeReminderFeature);
},
onClose: () => {
props.navigation.navigate("FluidPanelsView" as any);
}
});
}
}, [activeReminderFeature]);
const showDatePicker = () => {
setDatePickerVisibility(true);
@@ -288,7 +243,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
Notifications.scheduleNotification(_reminder as Reminder);
Navigation.queueRoutesForUpdate();
useRelationStore.getState().update();
handleBackNavigation();
Navigation.goBack();
} catch (e) {
ToastManager.error(e as Error, undefined);
}
@@ -312,12 +267,12 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
<Header
title={reminder ? strings.editReminder() : strings.newReminder()}
canGoBack
onLeftMenuButtonPress={handleBackNavigation}
rightButton={{
name: "check",
onPress: saveReminder
}}
/>
<Dialog context="local" />
<ScrollView
style={{
marginBottom: DDS.isTab ? 25 : undefined,

View File

@@ -58,7 +58,6 @@ export type Settings = {
timeFormat: string;
fontScale: number;
markdownShortcuts: boolean;
virtualization?: boolean;
features: Record<any, any>;
loggedIn: boolean;
defaultLineHeight: number;
@@ -91,10 +90,9 @@ export type SavePayload = {
data?: string;
type?: "tiptap";
sessionHistoryId?: number;
ignoreEdit: boolean;
tabId: string;
pendingChanges?: boolean;
sourceNoteId?: string;
pendingChangesAt?: number;
};
export type AppState = {

View File

@@ -149,7 +149,7 @@ const showActionsheet = async () => {
}
};
type ContentMessage = { html: string };
type ContentMessage = { html: string; ignoreEdit: boolean };
export const useEditorEvents = (
editor: useEditorType,
@@ -190,9 +190,6 @@ export const useEditorEvents = (
const markdownShortcuts = useSettingStore(
(state) => state.settings.markdownShortcuts
);
const editorVirtualization = useSettingStore(
(state) => state.settings?.editorVirtualization
);
const tools = useDragState((state) => state.data);
useEffect(() => {
@@ -239,7 +236,6 @@ export const useEditorEvents = (
dayFormat: db.settings?.getDayFormat(),
fontScale,
markdownShortcuts,
virtualization: editorVirtualization,
features,
loggedIn,
defaultLineHeight
@@ -262,7 +258,6 @@ export const useEditorEvents = (
loading,
fontScale,
markdownShortcuts,
editorVirtualization,
loggedIn,
defaultLineHeight,
features
@@ -416,19 +411,16 @@ export const useEditorEvents = (
.getState()
.getNoteIdForTab(editorMessage.tabId);
const saveNoteId = editorMessage.noteId || noteId;
switch (editorMessage.type) {
case EditorEvents.content:
DatabaseLogger.log("EditorEvents.content");
editor.saveContent({
type: editorMessage.type,
content: editorMessage.value.html as string,
noteId: saveNoteId,
sourceNoteId: editorMessage.noteId,
noteId: noteId,
tabId: editorMessage.tabId,
pendingChanges: editorMessage.value?.pendingChanges,
pendingChangesAt: editorMessage.value?.pendingChangesAt
ignoreEdit: (editorMessage.value as ContentMessage).ignoreEdit,
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EditorEvents.title:
@@ -436,11 +428,10 @@ export const useEditorEvents = (
editor.saveContent({
type: editorMessage.type,
title: editorMessage.value?.title as string,
noteId: saveNoteId,
sourceNoteId: editorMessage.noteId,
noteId: noteId,
tabId: editorMessage.tabId,
pendingChanges: editorMessage.value?.pendingChanges,
pendingChangesAt: editorMessage.value?.pendingChangesAt
ignoreEdit: false,
pendingChanges: editorMessage.value?.pendingChanges
});
break;
case EditorEvents.logger:

View File

@@ -260,65 +260,29 @@ export const useEditor = (
id,
data,
type,
ignoreEdit,
sessionHistoryId: currentSessionHistoryId,
tabId,
pendingChanges,
sourceNoteId,
pendingChangesAt
pendingChanges
}: SavePayload) => {
if (currentNotes.current[id as string]?.readonly || readonly) return;
if (sourceNoteId && id && sourceNoteId !== id) {
DatabaseLogger.error(
new Error(
`Refused to save content of note ${sourceNoteId} into note ${id}`
)
);
return;
}
try {
if (id && !(await db.notes?.note(id))) {
if (useTabStore.getState().getNoteIdForTab(tabId) === id) {
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined,
spellCheckDisabled: false
}
});
}
await reset(tabId);
useTabStore.getState().updateTab(tabId, {
session: {
noteId: undefined,
noteLocked: undefined,
locked: undefined,
readonly: undefined,
scrollTop: undefined,
selection: undefined,
spellCheckDisabled: false
}
});
return;
}
let note = id ? await db.notes?.note(id) : undefined;
// A restored pending change can be older than what is already in the
// db (it was saved on another device, or the save actually went
// through and only the acknowledgement was lost). Applying it would
// roll the note back, so verify it is still the newest edit. Content
// and title are compared separately so that a newer title doesn't
// discard pending content, and vice versa.
if (pendingChanges && pendingChangesAt && note) {
const dateEdited = data
? note.contentId
? (await db.content?.get(note.contentId))?.dateEdited
: undefined
: note.dateEdited;
if (dateEdited && dateEdited > pendingChangesAt) {
DatabaseLogger.log(
`Discarding stale pending ${
data ? "content" : "title"
} for note ${id}: edited at ${dateEdited}, change captured at ${pendingChangesAt}`
);
return id;
}
}
const locked = note && (await db.vaults.itemExists(note));
if (note?.conflicted) {
@@ -342,6 +306,11 @@ export const useEditor = (
noteData.title = title;
if (ignoreEdit) {
DatabaseLogger.log("Ignoring edits...");
noteData.dateEdited = note?.dateEdited;
}
if (data) {
noteData.content = {
data: data,
@@ -352,9 +321,6 @@ export const useEditor = (
let saved = false;
setTimeout(() => {
if (saved) return;
// Don't report progress on a tab that has moved on to another note.
if (id && useTabStore.getState().getNoteIdForTab(tabId) !== id)
return;
commands.setStatus(
getFormattedDate(note ? note.dateEdited : Date.now(), "date-time"),
strings.saving(),
@@ -470,26 +436,14 @@ export const useEditor = (
}
}
if (id && pendingChanges) {
if (data) {
currentContents.current[id] = {
data: data,
type: "tiptap",
noteId: id
};
}
lastContentChangeTime.current[id] = Date.now();
// Push the restored change into the editor only if the note is
// actually open in a tab, and only into that tab.
const noteTabId = useTabStore.getState().getTabForNote(id);
if (noteTabId !== undefined) {
postMessage(NativeEvents.title, title || note?.title, noteTabId);
if (data) {
postMessage(NativeEvents.html, { data: data }, noteTabId);
}
currentNotes.current[id] = note;
}
if (
id &&
id === useTabStore.getState().getCurrentNoteId() &&
pendingChanges
) {
postMessage(NativeEvents.title, title || note?.title, tabId);
postMessage(NativeEvents.html, data, tabId);
currentNotes.current[id] = note;
}
if (!saveCount.current[tabId]) {
@@ -749,13 +703,12 @@ export const useEditor = (
await postMessage(NativeEvents.title, item.title, tabId);
overlay(false);
const updatedTab = useTabStore.getState().getTab(tabId!);
await postMessage(
NativeEvents.html,
{
data: currentContents.current[item.id]?.data || "",
scrollTop: updatedTab?.session?.scrollTop,
selection: updatedTab?.session?.selection,
scrollTop: tab?.session?.scrollTop,
selection: tab?.session?.selection,
searchResultIndex: event.searchResultIndex
},
tabId,
@@ -979,25 +932,24 @@ export const useEditor = (
title,
content,
type,
ignoreEdit,
noteId,
tabId,
pendingChanges,
sourceNoteId,
pendingChangesAt
pendingChanges
}: {
noteId?: string;
title?: string;
content?: string;
type: string;
ignoreEdit: boolean;
tabId: string;
pendingChanges?: boolean;
sourceNoteId?: string;
pendingChangesAt?: number;
}) => {
DatabaseLogger.log(
`saveContent... title: ${!!title}, content: ${!!content}, noteId: ${noteId}`
);
if (
ignoreEdit ||
lock.current ||
(currentLoadingNoteId.current &&
currentLoadingNoteId.current === noteId)
@@ -1006,6 +958,7 @@ export const useEditor = (
lock.current: ${lock.current}
currentLoadingNoteId.current: ${currentLoadingNoteId.current}
ignoreEdit: ${ignoreEdit}
`);
if (lock.current) {
setTimeout(() => {
@@ -1018,10 +971,7 @@ export const useEditor = (
return;
}
// A restored pending change is not a live edit: it may still be
// discarded as stale by saveNote, so it must not claim to be the newest
// content until it is actually written.
if (noteId && !pendingChanges) {
if (noteId) {
lastContentChangeTime.current[noteId] = Date.now();
localTabState.current?.setEditTime(noteId, Date.now());
localTabState?.current?.set(tabId, {
@@ -1029,7 +979,7 @@ export const useEditor = (
});
}
if (type === EditorEvents.content && noteId && !pendingChanges) {
if (type === EditorEvents.content && noteId) {
currentContents.current[noteId as string] = {
data: content,
type: "tiptap",
@@ -1042,17 +992,15 @@ export const useEditor = (
data: content,
type: "tiptap",
id: noteId,
ignoreEdit,
sessionHistoryId: noteId ? editorSessionHistory.get(noteId) : undefined,
tabId: tabId,
pendingChanges,
sourceNoteId,
pendingChangesAt
pendingChanges
};
withTimer(
`${noteId || tabId}:${type}`,
noteId || "newnote",
() => {
if (!params.id && !params.sourceNoteId) {
if (!params.id) {
params.id = useTabStore.getState().getNoteIdForTab(tabId);
}
if (onChange && params.data) {
@@ -1070,7 +1018,7 @@ export const useEditor = (
saveNote(params);
}
},
150
ignoreEdit ? 0 : 150
);
},
[editorSessionHistory, withTimer, onChange, saveNote]

View File

@@ -30,7 +30,6 @@ import SettingsService from "../../services/settings";
import useNavigationStore from "../../stores/use-navigation-store";
import { useNotes } from "../../stores/use-notes-store";
import { openEditor } from "../notes/common";
import { db } from "../../common/database";
export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
const [notes, loading] = useNotes();
@@ -60,8 +59,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
placeholder: strings.searchInRoute(route.name),
type: "note",
title: route.name,
route: route.name,
items: db.notes.all
route: route.name
});
}}
id={route.name}

View File

@@ -35,10 +35,7 @@ import Paragraph from "../../components/ui/typography/paragraph";
import { useDBItem } from "../../hooks/use-db-item";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import Navigation, { NavigationProps } from "../../services/navigation";
import {
createItemSelectionStore,
SelectionState
} from "../../stores/item-selection-store";
import { createItemSelectionStore } from "../../stores/item-selection-store";
import { updateNotebook } from "../../utils/notebooks";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
@@ -50,12 +47,14 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
const currentNotebook = props.route.params.notebook;
const inputRef = useRef<TextInput>(null);
const [loading, setLoading] = useState(false);
const hasPendingChanges = useItemSelectionStore((state) => {
return Object.keys(state.selection).some((id) => {
return state.selection[id] !== state.initialState[id];
});
});
const selectionCount = useItemSelectionStore(
(state) =>
Object.keys(state.selection).filter(
(k) =>
state.selection?.[k] === "selected" ||
state.selection[k] === "deselected"
)?.length > 0
);
useNavigationFocus(props.navigation, { focusOnInit: true });
@@ -77,6 +76,20 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
.catch(() => {
setLoading(false);
});
db.relations
.from(currentNotebook, "note")
.get()
.then((existingNotes) => {
const selection: { [name: string]: any } = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection: selection,
initialState: selection
});
});
},
[currentNotebook]
);
@@ -84,25 +97,6 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
useEffect(() => {
loadNotes();
const loadSelection = async () => {
const existingNotes = await db.relations
.from(currentNotebook, "note")
.get();
const selection: Record<string, SelectionState> = {};
existingNotes.forEach((rel) => {
selection[rel.toId] = "selected";
});
useItemSelectionStore.setState({
selection,
initialState: selection
});
};
loadSelection();
return () => {
useItemSelectionStore.getState().reset();
};
@@ -165,8 +159,6 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
/>
<FlatList
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
ListEmptyComponent={
<View
style={{
@@ -184,19 +176,17 @@ export const MoveNotes = (props: NavigationProps<"MoveNotes">) => {
)}
</View>
}
style={{ flex: 1 }}
contentContainerStyle={{
paddingBottom: hasPendingChanges ? 80 : 0
style={{
flexGrow: 1
}}
data={loading ? [] : notes?.placeholders}
renderItem={renderItem}
/>
{hasPendingChanges ? (
{selectionCount ? (
<FloatingButton
icon="check"
alwaysVisible
hideOnKeyboard={false}
onPress={async () => {
await db.notes?.addToNotebook(
currentNotebook.id,

View File

@@ -46,7 +46,7 @@ export function toCamelCase(title: string) {
export function openMonographsWebpage() {
try {
openLinkInBrowser(
"https://notesnook.com/help/publish-notes-with-monographs"
"https://help.notesnook.com/publish-notes-with-monographs"
);
} catch (e) {
console.error(e);

View File

@@ -28,7 +28,6 @@ import {
} from "../../components/dialogs/progress";
import Navigation from "../../services/navigation";
import BackupService from "../../services/backup";
import { useUserStore } from "../../stores/use-user-store";
export async function logoutUser() {
const hasUnsyncedChanges = await db.hasUnsyncedChanges();
@@ -48,7 +47,6 @@ export async function logoutUser() {
: undefined,
positivePress: async (_, takeBackup) => {
eSendEvent(eCloseSimpleDialog);
useUserStore.getState().setIsLoggingOut(true);
setTimeout(async () => {
try {
startProgress({
@@ -106,7 +104,6 @@ export async function logoutUser() {
DatabaseLogger.error(e);
ToastManager.error(e as Error, strings.logoutError());
endProgress();
useUserStore.getState().setIsLoggingOut(false);
}
}, 300);
}

View File

@@ -137,8 +137,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] as TrashCleanupInterval[],
@@ -294,10 +294,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],
@@ -321,8 +321,8 @@ export const VaultLockTimerPicker = createSettingsPicker({
return item === -1
? strings.never()
: item < 1000 * 60 * 60
? strings.minutes(item / (1000 * 60))
: strings.hours(item / (1000 * 60 * 60));
? strings.minutes(item / (1000 * 60))
: strings.hours(item / (1000 * 60 * 60));
},
getItemKey: (item) => item.toString(),
options: [
@@ -349,8 +349,8 @@ export const ImageCompressionPicker = createSettingsPicker({
return item === "ask-every-time"
? strings.askEveryTime()
: item === "enabled"
? strings.enableRecommended()
: strings.disable();
? strings.enableRecommended()
: strings.disable();
},
getItemKey: (item) => item,
options: [

View File

@@ -208,15 +208,15 @@ export const settingsGroups: SettingSection[] = [
return status === SubscriptionStatus.TRIAL
? strings.trialOnGoing(trialEndDate)
: status === SubscriptionStatus.ACTIVE
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
? strings.subRenewOn(expiryDate)
: status === SubscriptionStatus.CANCELED ||
status === SubscriptionStatus.PAUSED
? strings.subEndsOn(expiryDate)
: status === SubscriptionStatus.EXPIRED
? subscriptionDaysLeft.time < -3
? strings.subEnded()
: strings.accountDowngradedIn(3)
: strings.neverHesitate();
}
return strings.neverHesitate();
@@ -971,7 +971,7 @@ export const settingsGroups: SettingSection[] = [
name: strings.keepScreenOn(),
description: strings.keepScreenOnDesc(),
property: "keepScreenOn",
icon: "cellphone-screenshot"
icon: "cellphone-screenshot",
},
{
id: "image-compression",
@@ -1023,14 +1023,6 @@ export const settingsGroups: SettingSection[] = [
});
}
},
{
id: "editor-virtualization",
name: strings.editorVirtualization(),
description: strings.editorVirtualizationDesc(),
type: "switch",
property: "editorVirtualization",
icon: "page-next-outline"
},
{
id: "default-font-size",
name: strings.defaultFontSize(),
@@ -1687,7 +1679,7 @@ export const settingsGroups: SettingSection[] = [
id: "docs-link",
name: strings.documentation(),
modifer: async () => {
Linking.openURL("https://notesnook.com/help/");
Linking.openURL("https://help.notesnook.com/");
},
description: strings.documentationDesc(),
icon: "file-document"

View File

@@ -472,7 +472,7 @@ function ThemeSelector() {
actionText: strings.learnMore(),
func: () => {
openLinkInBrowser(
"https://notesnook.com/help/custom-themes/introduction"
"https://help.notesnook.com/custom-themes/introduction"
);
}
});

View File

@@ -177,11 +177,9 @@ function resetRootState(
if (state.routes.length < 2) return;
let routes = state.routes.filter(
const routes = state.routes.filter(
(route) =>
(route.name !== "Auth" &&
route.name !== "Welcome" &&
route.name !== "AddReminder") ||
(route.name !== "Auth" && route.name !== "Welcome") ||
route.key === focusedRoute.key
);

View File

@@ -34,10 +34,6 @@ export const NotePreviewWidget = {
NotesnookModule.updateWidgetNote(id, JSON.stringify(newNote));
}
// Redraw from the widgets that actually exist rather than only the ones we
// have notes for. After app data is cleared there are none, and the widgets
// would otherwise keep showing content that no longer exists.
NotesnookModule.refreshWidgets();
}, 500);
},
updateNote: async (id: string, note: Note) => {

View File

@@ -17,12 +17,8 @@ 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 { getFormattedDate, getFormattedReminderTime } from "@notesnook/common";
import {
getUpcomingReminderTime,
isReminderActive,
Reminder
} from "@notesnook/core";
import { getFormattedReminderTime } from "@notesnook/common";
import { isReminderActive, Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import notifee, {
AndroidStyle,
@@ -266,13 +262,8 @@ const onEvent = async ({ type, detail }: Event) => {
type ReminderWithFormattedTime = Reminder & {
formattedTime?: string;
triggerDate?: number;
formattedTimeOfDay?: string;
formattedDateTime?: string;
};
const RECENTLY_PASSED_WINDOW = 3 * 60 * 60 * 1000;
async function updateRemindersForWidget() {
if (Platform.OS === "ios") return;
const reminders: ReminderWithFormattedTime[] = await db.reminders?.all.items(
@@ -282,33 +273,18 @@ async function updateRemindersForWidget() {
sortDirection: "asc"
}
);
const widgetReminders = [];
const activeReminders = [];
if (!reminders) return;
for (const reminder of reminders) {
const triggerDate =
reminder.snoozeUntil && reminder.snoozeUntil > Date.now()
? reminder.snoozeUntil
: reminder.mode === "repeat"
? getUpcomingReminderTime(reminder)
: reminder.date;
const recentlyPassed =
reminder.mode === "once" &&
!reminder.disabled &&
triggerDate > Date.now() - RECENTLY_PASSED_WINDOW;
if (!isReminderActive(reminder) && !recentlyPassed) continue;
reminder.triggerDate = triggerDate;
reminder.formattedTimeOfDay = getFormattedDate(triggerDate, "time");
reminder.formattedDateTime = getFormattedDate(triggerDate, "date-time");
reminder.formattedTime = getFormattedReminderTime(reminder);
widgetReminders.push(reminder);
if (isReminderActive(reminder)) {
reminder.formattedTime = getFormattedReminderTime(reminder);
activeReminders.push(reminder);
}
}
NotesnookModule.setString(
"appPreview",
"remindersList",
JSON.stringify(widgetReminders)
JSON.stringify(activeReminders)
);
NotesnookModule.updateReminderWidget();
}

View File

@@ -63,11 +63,6 @@ const run = async (
clearTimeout(syncTimer);
syncTimer = setTimeout(async () => {
if (useUserStore.getState().isLoggingOut) {
DatabaseLogger.info("Sync skipped — user is logging out");
return;
}
const userstore = useUserStore.getState();
userstore.setSyncing(true);
const user = await db.user.getUser();

View File

@@ -711,10 +711,9 @@ const ShareView = () => {
width: "100%",
marginTop: 6
}}
onPress={async () => {
const feature = await isFeatureAvailable("fullQualityImages");
if (feature?.isAllowed) {
setCompress((prev) => !prev);
onPress={() => {
if (fullQualityImages?.isAllowed) {
setCompress(!compress);
}
}}
>

View File

@@ -56,14 +56,17 @@ export function createItemSelectionStore(
canEnableMultiSelectMode: multiSelectMode,
initialState: {},
markAs: (item, state) => {
const selection = { ...get().selection };
const initial = get().initialState[item.id];
if (state === "deselected" && initial === undefined) {
delete selection[item.id];
} else {
selection[item.id] = state;
}
set({ selection });
set({
selection: {
...get().selection,
[item.id]:
state === "deselected"
? get().initialState === undefined
? undefined
: "deselected"
: state
}
});
},
multiSelect: false,
toggleMultiSelect: () => {

View File

@@ -74,21 +74,13 @@ export interface RouteParams extends ParamListBase {
Tags: GenericRouteParam;
Favorites: GenericRouteParam;
Trash: GenericRouteParam;
Search:
| {
placeholder: string;
type: "note";
title: string;
route: RouteName;
items: FilteredSelector<Note>;
}
| {
placeholder: string;
type: Exclude<ItemType, "note">;
title: string;
route: RouteName;
items?: FilteredSelector<Item>;
};
Search: {
placeholder: string;
type: ItemType;
title: string;
route: RouteName;
items?: FilteredSelector<Item>;
};
TaggedNotes: NotesScreenParams;
ColoredNotes: NotesScreenParams;
TopicNotes: NotesScreenParams;
@@ -96,7 +88,7 @@ export interface RouteParams extends ParamListBase {
Monographs: NotesScreenParams;
Reminders: GenericRouteParam;
SettingsGroup: GenericRouteParam;
FluidPanelsView: { initialPage?: "editor" | "home" };
FluidPanelsView: GenericRouteParam;
AppLock: GenericRouteParam;
Settings: GenericRouteParam;
Auth: AuthParams;

View File

@@ -27,7 +27,6 @@ import { ThemeDark, ThemeLight, ThemeDefinition } from "@notesnook/theme";
import { DayFormat, WeekFormat, Reminder } from "@notesnook/core";
import { db } from "../common/database";
import { EDITOR_LINE_HEIGHT } from "../utils/constants";
import { ShortcutItem } from "react-native-actions-shortcuts";
export const HostIds = [
"API_HOST",
"AUTH_HOST",
@@ -78,7 +77,6 @@ export type Settings = {
sessionExpired: boolean;
version: string | null;
doubleSpacedLines?: boolean;
editorVirtualization?: boolean;
disableAutoSync?: boolean;
disableSync?: boolean;
reminderNotifications?: boolean;
@@ -151,7 +149,6 @@ export interface SettingStore {
refresh: () => void;
inboxEnabled: boolean;
setInboxEnabled: (inboxEnabled: boolean) => void;
pendingShortcut: ShortcutItem | null;
}
const { width, height } = Dimensions.get("window");
@@ -197,7 +194,6 @@ export const defaultSettings: SettingStore["settings"] = {
sessionExpired: false,
version: null,
doubleSpacedLines: true,
editorVirtualization: false,
reminderNotifications: true,
defaultSnoozeTime: "5",
corsProxy: "https://cors.notesnook.com",
@@ -273,6 +269,5 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
});
},
inboxEnabled: false,
setInboxEnabled: (inboxEnabled) => set({ inboxEnabled }),
pendingShortcut: null
setInboxEnabled: (inboxEnabled) => set({ inboxEnabled })
}));

View File

@@ -48,8 +48,6 @@ export interface UserStore {
disableAppLockRequests: boolean;
setDisableAppLockRequests: (disableAppLockRequests: boolean) => void;
profile?: Partial<Profile>;
isLoggingOut: boolean;
setIsLoggingOut: (value: boolean) => void;
}
export const useUserStore = create<UserStore>((set) => ({
@@ -97,7 +95,5 @@ export const useUserStore = create<UserStore>((set) => ({
set({ disableAppLockRequests: false });
}, 1000);
},
profile: undefined,
isLoggingOut: false,
setIsLoggingOut: (value) => set({ isLoggingOut: value })
profile: undefined
}));

View File

@@ -45,7 +45,6 @@ interface NotesnookModuleInterface {
hasWidgetNote: (noteId: string) => Promise<boolean>;
updateWidgetNote: (noteId: string, data: string) => void;
updateReminderWidget: () => void;
refreshWidgets: () => void;
isGestureNavigationEnabled: () => boolean;
addShortcut: (
id: string,
@@ -82,7 +81,6 @@ export const NotesnookModule: NotesnookModuleInterface = Platform.select({
hasWidgetNote: () => {},
updateWidgetNote: () => {},
updateReminderWidget: () => {},
refreshWidgets: () => {},
isGestureNavigationEnabled: () => true,
addShortcut: () => Promise.resolve(false),
removeShortcut: () => Promise.resolve(false),

View File

@@ -5,8 +5,6 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip
require_relative 'scripts/patch_fmt_consteval'
platform :ios, min_ios_version_supported
prepare_react_native_project!
@@ -68,10 +66,6 @@ post_install do |installer|
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)
# Keep fmt buildable on Xcode >= 26.2. See ios/scripts/patch_fmt_consteval.rb.
PatchFmtConsteval.apply!(installer.sandbox.root)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'

View File

@@ -4092,6 +4092,6 @@ SPEC CHECKSUMS:
toolbar-android: c426ed5bd3dcccfed20fd79533efc0d1ae0ef018
Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb
PODFILE CHECKSUM: 30b2045c0f4fc91402a43a9e2a872af803f2d6c3
PODFILE CHECKSUM: 3fe13efa8356dcc061862bfa9f453dcd12ede70a
COCOAPODS: 1.16.2

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2194
IOS_MARKETING_VERSION = 3.4.10
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Production iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2194
IOS_MARKETING_VERSION = 3.4.10
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,6 +1,6 @@
// Staging iOS build identifiers
IOS_CURRENT_PROJECT_VERSION = 2194
IOS_MARKETING_VERSION = 3.4.10
IOS_CURRENT_PROJECT_VERSION = 2191
IOS_MARKETING_VERSION = 3.4.7
IOS_MAIN_BUNDLE_ID = org.streetwriters.notesnook
IOS_WIDGET_BUNDLE_ID = org.streetwriters.notesnook.notewidget
IOS_SHARE_BUNDLE_ID = org.streetwriters.notesnook.share

View File

@@ -1,77 +0,0 @@
# Xcode >= 26.2 rejects fmt's compile-time format-string check with
#
# call to consteval function 'fmt::fstring<...>::fstring<char[N]>' is not a
# constant expression
#
# React Native 0.81/0.82 vendor fmt 11.0.2, which hits this. It is an upstream
# incompatibility (reproducible in a stock RN app), but left alone it makes those
# RN versions permanently un-buildable on a modern Xcode.
#
# fmt's own escape hatch is FMT_USE_CONSTEVAL: 0 downgrades the format-string
# check from compile-time to run-time. fmt 11.0.2 does not guard its detection
# block with #ifndef, so we cannot simply predefine the macro -- and doing it
# through the build settings is worse anyway:
#
# * a command-line GCC_PREPROCESSOR_DEFINITIONS outranks every per-target
# value, silently dropping COCOAPODS=1, RCT_METRO_PORT, ...
# * a second `post_install` block in the Podfile REPLACES React Native's own.
#
# So we patch the header itself, right after fmt has made up its mind and before
# the first use of the macro. Idempotent, and safe to run on every pod install.
module PatchFmtConsteval
MARKER = 'NOTESNOOK_FMT_CONSTEVAL_PATCH'.freeze
# The line that first consumes the macro; our override goes immediately above
# it, i.e. after the whole detection cascade.
ANCHOR = "#if FMT_USE_CONSTEVAL\n".freeze
OVERRIDE = <<~PATCH.freeze
// #{MARKER}: Xcode >= 26.2 rejects fmt's consteval format-string check
// (see ios/scripts/patch_fmt_consteval.rb). Applied automatically by
// `pod install`; downgrades the check to run-time.
#undef FMT_USE_CONSTEVAL
#define FMT_USE_CONSTEVAL 0
PATCH
# pods_root: the Pods directory (installer.sandbox.root).
def self.apply!(pods_root)
header = File.join(pods_root.to_s, 'fmt', 'include', 'fmt', 'base.h')
unless File.exist?(header)
Pod::UI.warn "fmt: #{header} not found, skipping consteval patch."
return
end
contents = File.read(header)
if contents.include?(MARKER)
Pod::UI.puts 'fmt: consteval patch already applied.'
return
end
index = contents.index(ANCHOR)
if index.nil?
Pod::UI.warn 'fmt: could not find `#if FMT_USE_CONSTEVAL` in base.h; ' \
'the consteval patch was NOT applied. If this fmt version ' \
'still uses a consteval format-string check, builds on ' \
'Xcode >= 26.2 will fail -- update ' \
'ios/scripts/patch_fmt_consteval.rb.'
return
end
contents.insert(index, OVERRIDE)
# CocoaPods checks pod sources out read-only (0444), so make the header
# writable for the write and restore the original mode afterwards.
mode = File.stat(header).mode & 0o7777
begin
File.chmod(mode | 0o200, header)
File.write(header, contents)
ensure
File.chmod(mode, header)
end
Pod::UI.puts 'fmt: patched base.h to set FMT_USE_CONSTEVAL=0 (Xcode >= 26.2 compatibility).'
end
end

View File

@@ -1,12 +1,12 @@
{
"name": "@notesnook/mobile",
"version": "3.4.10",
"version": "3.4.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@notesnook/mobile",
"version": "3.4.10",
"version": "3.4.5",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -191,7 +191,6 @@
"version": "2.1.3",
"license": "GPL-3.0-or-later",
"dependencies": {
"@notesnook/common": "^2.1.3",
"@notesnook/core": "file:../core",
"@readme/data-urls": "^3.0.0",
"dayjs": "1.11.13",
@@ -334,10 +333,7 @@
"papaparse": "^5.5.3",
"prism-themes": "^1.9.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-model": "1.25.11",
"prosemirror-state": "1.4.4",
"prosemirror-transform": "1.12.0",
"prosemirror-view": "1.42.2",
"prosemirror-view": "1.34.2",
"re-resizable": "^6.9.18",
"react-colorful": "^5.6.1",
"redent": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@notesnook/mobile",
"version": "3.4.10",
"version": "3.4.7",
"private": true,
"license": "GPL-3.0-or-later",
"scripts": {

View File

@@ -1,98 +1,8 @@
diff --git a/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt b/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt
index 70149d2..cc2b272 100644
index 70149d2..fc917f3 100644
--- a/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt
+++ b/node_modules/react-native-iap/android/src/play/java/com/dooboolab/rniap/RNIapModule.kt
@@ -15,10 +15,8 @@ import com.android.billingclient.api.GetBillingConfigParams
import com.android.billingclient.api.GetBillingConfigParams.Builder
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.Purchase
-import com.android.billingclient.api.PurchaseHistoryRecord
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
-import com.android.billingclient.api.QueryPurchaseHistoryParams
import com.android.billingclient.api.QueryPurchasesParams
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.LifecycleEventListener
@@ -38,11 +36,14 @@ import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
+import com.android.billingclient.api.PendingPurchasesParams
@ReactModule(name = RNIapModule.TAG)
class RNIapModule(
private val reactContext: ReactApplicationContext,
- private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(),
+ private val builder: BillingClient.Builder = BillingClient.newBuilder(reactContext).enablePendingPurchases(
+ PendingPurchasesParams.newBuilder().enableOneTimeProducts().build(),
+),
private val googleApiAvailability: GoogleApiAvailability = GoogleApiAvailability.getInstance(),
) : ReactContextBaseJavaModule(reactContext),
PurchasesUpdatedListener {
@@ -275,8 +276,10 @@ class RNIapModule(
.setProductList(skuList)
.build()
- billingClient.queryProductDetailsAsync(params) { billingResult, skuDetailsList ->
- if (!isValidResult(billingResult, promise)) return@queryProductDetailsAsync
+ billingClient.queryProductDetailsAsync(params) { billingResult, queryProductDetailsResult ->
+ if (!isValidResult(billingResult, promise)) return@queryProductDetailsAsync
+
+ val skuDetailsList = queryProductDetailsResult.productDetailsList
val items = Arguments.createArray()
for (skuDetails in skuDetailsList) {
@@ -553,43 +556,12 @@ class RNIapModule(
type: String,
promise: Promise,
) {
- ensureConnection(
- promise,
- ) { billingClient ->
- billingClient.queryPurchaseHistoryAsync(
- QueryPurchaseHistoryParams
- .newBuilder()
- .setProductType(
- if (type == "subs") BillingClient.ProductType.SUBS else BillingClient.ProductType.INAPP,
- ).build(),
- ) { billingResult: BillingResult, purchaseHistoryRecordList: MutableList<PurchaseHistoryRecord>? ->
-
- if (!isValidResult(billingResult, promise)) return@queryPurchaseHistoryAsync
-
- Log.d(TAG, purchaseHistoryRecordList.toString())
- val items = Arguments.createArray()
- purchaseHistoryRecordList?.forEach { purchase ->
- val item = Arguments.createMap()
- // Add both field names for compatibility
- item.putString("productId", purchase.products[0])
- item.putString("id", purchase.products[0])
- val products = Arguments.createArray()
- purchase.products.forEach { products.pushString(it) }
- item.putArray("productIds", products)
- item.putArray("ids", products)
- item.putDouble("transactionDate", purchase.purchaseTime.toDouble())
- item.putString("transactionReceipt", purchase.originalJson)
- item.putString("purchaseToken", purchase.purchaseToken)
- item.putString("purchaseTokenAndroid", purchase.purchaseToken)
- item.putString("dataAndroid", purchase.originalJson)
- item.putString("signatureAndroid", purchase.signature)
- item.putString("developerPayload", purchase.developerPayload.orEmpty())
- item.putString("platform", "android")
- items.pushMap(item)
- }
- promise.safeResolve(items)
- }
- }
+ promise.safeReject(
+ "E_UNSUPPORTED",
+ "getPurchaseHistoryByType is no longer supported since Play Billing Library 8 " +
+ "removed queryPurchaseHistoryAsync. Use getAvailableItemsByType for active " +
+ "purchases, or reconstruct history server-side.",
+ )
}
@ReactMethod
@@ -604,7 +576,7 @@ class RNIapModule(
@@ -604,7 +604,7 @@ class RNIapModule(
isOfferPersonalized: Boolean, // New parameter in V5
promise: Promise,
) {

View File

@@ -128,8 +128,7 @@ const EXTRA_ICON_NAMES = [
"identifier",
"image-area",
"clock-outline",
"delete-sweep-outline",
"sync"
"delete-sweep-outline"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -141,7 +141,7 @@ export default function Monograph() {
bg: "background-secondary",
border: "1px solid var(--border)"
}}
href="https://notesnook.com/help/publish-notes-with-monographs"
href="https://help.notesnook.com/publish-notes-with-monographs"
target="_blank"
>
How it works

View File

@@ -33,6 +33,7 @@ import {
} from "./utils";
import { NavigationMenuModel } from "./navigation-menu.model";
import { AppModel } from "./app.model";
import { getAppFromPage } from "../../../desktop/__tests__/electron-test/utils";
import { readFile } from "node:fs/promises";
export class SettingsViewModel {
@@ -134,10 +135,6 @@ export class SettingsViewModel {
};
if (IS_DESKTOP_TESTS) {
const { getAppFromPage } = await import(
"../../../desktop/__tests__/electron-test/utils"
);
await saveBackup();
const toast = new AppModel(this.page).toasts.toasts.locator(
getTestId("toast-message")

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@notesnook/web",
"description": "Your private note taking space",
"version": "3.4.6",
"version": "3.4.4",
"private": true,
"main": "./src/app.js",
"homepage": "https://notesnook.com/",

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useMemo } from "react";
import { Button, Flex, Image, Text } from "@theme-ui/components";
import { Box, Button, Flex, Image, Link, Text } from "@theme-ui/components";
import { getRandom, usePromise } from "@notesnook/common";
import Holenstein from "../../assets/testimonials/holenstein.jpg";
import Jason from "../../assets/testimonials/jason.jpg";
@@ -26,7 +26,6 @@ import Cameron from "../../assets/testimonials/cameron.jpg";
import { hosts } from "@notesnook/core";
import { SettingsDialog } from "../../dialogs/settings";
import { strings } from "@notesnook/intl";
import { FixedColorSchemeThemeProvider } from "../theme-provider";
const testimonials = [
{
@@ -81,18 +80,62 @@ function AuthContainer(props) {
bg: "background"
}}
>
<FixedColorSchemeThemeProvider
colorScheme="dark"
<Box
sx={{
position: "relative",
overflow: "hidden",
flexDirection: "column",
display: ["none", "none", "flex"],
flex: 1,
background:
"radial-gradient(1200px 700px at 82% 18%, color-mix(in srgb, var(--accent) 14%, transparent) 0%, transparent 62%), var(--background-secondary)"
flex: 1
}}
>
<Box
as="svg"
version="1.1"
viewBox="0 0 1920 1080"
preserveAspectRatio="xMinYMin slice"
sx={{
position: "absolute",
top: -100,
left: 0,
height: "100%"
// opacity: 0.7,
}}
>
<g mask='url("#SvgjsMask1017")' fill="none">
<path
d="M1184.21-85.14C1033.8-60.27 964.89 302.42 717.38 307.22 469.87 312.02 483.97 244.72 250.55 244.72 17.13 244.72-98.53 307.08-216.28 307.22"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M641.38-10.43C534.57 43 590.55 387.5 384.53 392.38 178.52 397.26 2.17 282.99-129.16 282.38"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M1136.18-29.24C957.53-5.77 852.26 404.49 561.01 405.07 269.76 405.65 142.54 160.4-14.16 155.07"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M508.47-71.88C398.16-66.29 333.42 117.75 114.38 127.84-104.65 137.93-170.96 308.31-279.7 312.84"
stroke="var(--icon)"
strokeWidth="2"
></path>
<path
d="M1104.88-26.74C976.63-19.04 883.5 217.2 653.03 218.11 422.55 219.02 427.1 155.61 201.17 155.61-24.75 155.61-136.64 217.96-250.68 218.11"
stroke="var(--icon)"
strokeWidth="2"
></path>
</g>
<defs>
<mask id="SvgjsMask1017">
<rect width="1440" height="500" fill="#ffffff"></rect>
</mask>
</defs>
</Box>
<Flex
p={50}
sx={{
@@ -119,9 +162,17 @@ function AuthContainer(props) {
<Text
variant="body"
mt={10}
sx={{ fontSize: 16, color: "paragraph-secondary" }}
sx={{ fontSize: 14, color: "paragraph-secondary" }}
>
{testimonial.text}
{testimonial.text} {" "}
<Link
sx={{ fontStyle: "italic", color: "paragraph-secondary" }}
href={testimonial.link}
target="_blank"
rel="noopener noreferrer"
>
source
</Link>
</Text>
<Flex mt={2} sx={{ alignItems: "center", justifyContent: "center" }}>
<Image
@@ -129,12 +180,10 @@ function AuthContainer(props) {
sx={{ borderRadius: 50, width: 40 }}
/>
<Flex ml={2} sx={{ flexDirection: "column" }}>
<Text variant="body" sx={{ fontSize: 16, fontWeight: "bold" }}>
<Text variant="body" sx={{ fontSize: 14, fontWeight: "bold" }}>
{testimonial.name}
</Text>
<Text variant="subBody" sx={{ fontSize: 13 }}>
@{testimonial.username}
</Text>
<Text variant="subBody">@{testimonial.username}</Text>
</Flex>
</Flex>
@@ -169,18 +218,52 @@ function AuthContainer(props) {
</Button>
</Flex>
</Flex>
</FixedColorSchemeThemeProvider>
<FixedColorSchemeThemeProvider
colorScheme="light"
</Box>
<Flex
sx={{
display: "flex",
position: "relative",
flex: 1.5,
background: "var(--background-secondary)"
flexDirection: "column"
}}
>
<Box
as="svg"
version="1.1"
viewBox="0 0 1920 1080"
preserveAspectRatio="xMinYMin slice"
sx={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
width: "130%",
height: "100%"
}}
>
<path
d="M0 336L29.2 316.2C58.3 296.3 116.7 256.7 174.8 267.5C233 278.3 291 339.7 349.2 361.3C407.3 383 465.7 365 523.8 359.5C582 354 640 361 698.2 346.5C756.3 332 814.7 296 872.8 267.2C931 238.3 989 216.7 1047.2 202.3C1105.3 188 1163.7 181 1221.8 202.7C1280 224.3 1338 274.7 1396.2 298C1454.3 321.3 1512.7 317.7 1570.8 332C1629 346.3 1687 378.7 1745.2 366.2C1803.3 353.7 1861.7 296.3 1890.8 267.7L1920 239L1920 0L1890.8 0C1861.7 0 1803.3 0 1745.2 0C1687 0 1629 0 1570.8 0C1512.7 0 1454.3 0 1396.2 0C1338 0 1280 0 1221.8 0C1163.7 0 1105.3 0 1047.2 0C989 0 931 0 872.8 0C814.7 0 756.3 0 698.2 0C640 0 582 0 523.8 0C465.7 0 407.3 0 349.2 0C291 0 233 0 174.8 0C116.7 0 58.3 0 29.2 0L0 0Z"
fill="var(--background-secondary)"
></path>
<path
d="M0 627L29.2 607.3C58.3 587.7 116.7 548.3 174.8 564.7C233 581 291 653 349.2 683.5C407.3 714 465.7 703 523.8 703C582 703 640 714 698.2 724.8C756.3 735.7 814.7 746.3 872.8 742.7C931 739 989 721 1047.2 670.7C1105.3 620.3 1163.7 537.7 1221.8 528.7C1280 519.7 1338 584.3 1396.2 623.8C1454.3 663.3 1512.7 677.7 1570.8 666.8C1629 656 1687 620 1745.2 602C1803.3 584 1861.7 584 1890.8 584L1920 584L1920 237L1890.8 265.7C1861.7 294.3 1803.3 351.7 1745.2 364.2C1687 376.7 1629 344.3 1570.8 330C1512.7 315.7 1454.3 319.3 1396.2 296C1338 272.7 1280 222.3 1221.8 200.7C1163.7 179 1105.3 186 1047.2 200.3C989 214.7 931 236.3 872.8 265.2C814.7 294 756.3 330 698.2 344.5C640 359 582 352 523.8 357.5C465.7 363 407.3 381 349.2 359.3C291 337.7 233 276.3 174.8 265.5C116.7 254.7 58.3 294.3 29.2 314.2L0 334Z"
fill="var(--hover)"
></path>
<path
d="M0 735L29.2 731.5C58.3 728 116.7 721 174.8 739C233 757 291 800 349.2 832.3C407.3 864.7 465.7 886.3 523.8 886.3C582 886.3 640 864.7 698.2 859.3C756.3 854 814.7 865 872.8 870.5C931 876 989 876 1047.2 845.3C1105.3 814.7 1163.7 753.3 1221.8 729.8C1280 706.3 1338 720.7 1396.2 738.7C1454.3 756.7 1512.7 778.3 1570.8 789.2C1629 800 1687 800 1745.2 814.5C1803.3 829 1861.7 858 1890.8 872.5L1920 887L1920 582L1890.8 582C1861.7 582 1803.3 582 1745.2 600C1687 618 1629 654 1570.8 664.8C1512.7 675.7 1454.3 661.3 1396.2 621.8C1338 582.3 1280 517.7 1221.8 526.7C1163.7 535.7 1105.3 618.3 1047.2 668.7C989 719 931 737 872.8 740.7C814.7 744.3 756.3 733.7 698.2 722.8C640 712 582 701 523.8 701C465.7 701 407.3 712 349.2 681.5C291 651 233 579 174.8 562.7C116.7 546.3 58.3 585.7 29.2 605.3L0 625Z"
fill="var(--border)"
></path>
<path
d="M0 897L29.2 895.3C58.3 893.7 116.7 890.3 174.8 908.3C233 926.3 291 965.7 349.2 985.3C407.3 1005 465.7 1005 523.8 1003.3C582 1001.7 640 998.3 698.2 996.7C756.3 995 814.7 995 872.8 986C931 977 989 959 1047.2 939.2C1105.3 919.3 1163.7 897.7 1221.8 894C1280 890.3 1338 904.7 1396.2 911.8C1454.3 919 1512.7 919 1570.8 928C1629 937 1687 955 1745.2 960.3C1803.3 965.7 1861.7 958.3 1890.8 954.7L1920 951L1920 885L1890.8 870.5C1861.7 856 1803.3 827 1745.2 812.5C1687 798 1629 798 1570.8 787.2C1512.7 776.3 1454.3 754.7 1396.2 736.7C1338 718.7 1280 704.3 1221.8 727.8C1163.7 751.3 1105.3 812.7 1047.2 843.3C989 874 931 874 872.8 868.5C814.7 863 756.3 852 698.2 857.3C640 862.7 582 884.3 523.8 884.3C465.7 884.3 407.3 862.7 349.2 830.3C291 798 233 755 174.8 737C116.7 719 58.3 726 29.2 729.5L0 733Z"
fill="var(--hover)"
></path>
<path
d="M0 1081L29.2 1081C58.3 1081 116.7 1081 174.8 1081C233 1081 291 1081 349.2 1081C407.3 1081 465.7 1081 523.8 1081C582 1081 640 1081 698.2 1081C756.3 1081 814.7 1081 872.8 1081C931 1081 989 1081 1047.2 1081C1105.3 1081 1163.7 1081 1221.8 1081C1280 1081 1338 1081 1396.2 1081C1454.3 1081 1512.7 1081 1570.8 1081C1629 1081 1687 1081 1745.2 1081C1803.3 1081 1861.7 1081 1890.8 1081L1920 1081L1920 949L1890.8 952.7C1861.7 956.3 1803.3 963.7 1745.2 958.3C1687 953 1629 935 1570.8 926C1512.7 917 1454.3 917 1396.2 909.8C1338 902.7 1280 888.3 1221.8 892C1163.7 895.7 1105.3 917.3 1047.2 937.2C989 957 931 975 872.8 984C814.7 993 756.3 993 698.2 994.7C640 996.3 582 999.7 523.8 1001.3C465.7 1003 407.3 1003 349.2 983.3C291 963.7 233 924.3 174.8 906.3C116.7 888.3 58.3 891.7 29.2 893.3L0 895Z"
fill="var(--border)"
></path>
</Box>
{props.children}
</FixedColorSchemeThemeProvider>
</Flex>
</Flex>
);
}

Some files were not shown because too many files have changed in this diff Show More