mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-31 19:19:34 +02:00
Compare commits
18 Commits
fix-perman
...
2.4.1-andr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f69c5b268 | ||
|
|
215a692139 | ||
|
|
f1b986cef9 | ||
|
|
da0f86c006 | ||
|
|
0a4e84b376 | ||
|
|
f1f8a35ea5 | ||
|
|
1ffb6b3c57 | ||
|
|
0d3504dca0 | ||
|
|
0f8f53d811 | ||
|
|
85aae88300 | ||
|
|
fc69b5ffd0 | ||
|
|
a7369bf869 | ||
|
|
2da32e27ff | ||
|
|
3e71ad0b0b | ||
|
|
a62ca2aada | ||
|
|
ffacb8818c | ||
|
|
7be867c7ca | ||
|
|
29f3dafc4f |
@@ -25,10 +25,7 @@ import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger, db, loadDatabase } from "../../common/database";
|
||||
import { useAppState } from "../../hooks/use-app-state";
|
||||
import BiometricService from "../../services/biometrics";
|
||||
import {
|
||||
eSendEvent,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { setRateAppMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -55,6 +52,8 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import Config from "react-native-config";
|
||||
import { getGithubVersion } from "../../utils/github-version";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
|
||||
const Launcher = React.memo(
|
||||
function Launcher() {
|
||||
@@ -133,6 +132,7 @@ const Launcher = React.memo(
|
||||
eSendEvent("session_expired");
|
||||
return;
|
||||
}
|
||||
notifee.setBadgeCount(0);
|
||||
await useMessageStore.getState().setAnnouncement();
|
||||
if (NewFeature.present()) return;
|
||||
if (await checkAppUpdateAvailable()) return;
|
||||
|
||||
@@ -94,9 +94,9 @@ NewFeature.present = () => {
|
||||
return;
|
||||
}
|
||||
if (version && version === getVersion()) return false;
|
||||
// SettingsService.set({
|
||||
// version: getVersion()
|
||||
// });
|
||||
SettingsService.set({
|
||||
version: getVersion()
|
||||
});
|
||||
const _features = features?.filter(
|
||||
(feature) => !feature.platform || feature.platform === Platform.OS
|
||||
);
|
||||
|
||||
@@ -19,25 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { FeatureType } from "./components/sheets/new-feature";
|
||||
|
||||
export const features: FeatureType[] = [
|
||||
{
|
||||
title: "Cross platform Reminders 🔔",
|
||||
body: "Finally reminders are here. You can set reminders on notes or independently. Go to Side Menu > Reminders to set your first reminder!"
|
||||
},
|
||||
{
|
||||
title: "Multi-factor auth by default",
|
||||
body: "Multi-factor auth is enabled by default for all users to improve login security."
|
||||
},
|
||||
{
|
||||
title: "Change account email",
|
||||
body: "Now you can go to Settings > Account Settings to change your email."
|
||||
},
|
||||
{
|
||||
title: "Sync settings",
|
||||
body: "Added options to disable sync, auto-sync & real-time sync from settings."
|
||||
},
|
||||
{
|
||||
title: "Improved PDF exports styling",
|
||||
body: "Tables, checklists, codeblocks, quotes are now properly formatted in PDF exports."
|
||||
}
|
||||
];
|
||||
export const features: FeatureType[] = [];
|
||||
|
||||
@@ -69,6 +69,7 @@ import { clearAppState } from "../screens/editor/tiptap/utils";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import BackupService from "../services/backup";
|
||||
import { sleep } from "../utils/time";
|
||||
import notifee from "@notifee/react-native";
|
||||
|
||||
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
|
||||
export const useAppEvents = () => {
|
||||
@@ -444,6 +445,7 @@ export const useAppEvents = () => {
|
||||
const onAppStateChanged = useCallback(
|
||||
async (state) => {
|
||||
if (state === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
updateStatusBarColor();
|
||||
if (
|
||||
SettingsService.get().appLockMode !== "background" &&
|
||||
|
||||
@@ -28,13 +28,14 @@ import { Button } from "../../components/ui/button";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeStore } from "../../stores/use-theme-store";
|
||||
import { eClearEditor, eOnLoadNote } from "../../utils/events";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { editorState } from "./tiptap/utils";
|
||||
const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
@@ -64,13 +65,8 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
opacity.value = 1;
|
||||
translateValue.value = 0;
|
||||
timers.current.error = setTimeout(() => {
|
||||
if (_loading) {
|
||||
let note = _loading;
|
||||
note.forced = true;
|
||||
eSendEvent(eOnLoadNote + editorId, note);
|
||||
}
|
||||
setError(true);
|
||||
}, 4000);
|
||||
}, 15 * 1000);
|
||||
} else {
|
||||
clearTimers();
|
||||
setTimeout(() => {
|
||||
@@ -85,7 +81,7 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[editorId, opacity, translateValue]
|
||||
[opacity, translateValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -144,15 +140,19 @@ const EditorOverlay = ({ editorId = "", editor }) => {
|
||||
paddingRight: 12
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
eSendEvent(eClearEditor);
|
||||
opacity.value = 0;
|
||||
translateValue.value = 6000;
|
||||
}}
|
||||
name="arrow-left"
|
||||
color={colors.pri}
|
||||
/>
|
||||
{DDS.isTablet() ? (
|
||||
<View />
|
||||
) : (
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
eSendEvent(eClearEditor);
|
||||
opacity.value = 0;
|
||||
translateValue.value = 6000;
|
||||
}}
|
||||
name="arrow-left"
|
||||
color={colors.pri}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -112,7 +112,7 @@ export const useEditor = (
|
||||
(show: boolean, data = { type: "new" }) => {
|
||||
eSendEvent(
|
||||
"loadingNote" + editorId,
|
||||
show ? currentNote.current || data : false
|
||||
show ? data || currentNote.current : false
|
||||
);
|
||||
},
|
||||
[editorId]
|
||||
|
||||
121
apps/mobile/app/services/background-sync.ts
Normal file
121
apps/mobile/app/services/background-sync.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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 BackgroundFetch from "react-native-background-fetch";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { AppState, AppRegistry } from "react-native";
|
||||
import Notifications from "./notifications";
|
||||
import SettingsService from "./settings";
|
||||
|
||||
let backgroundFetchStarted = false;
|
||||
async function start() {
|
||||
if (backgroundFetchStarted) return;
|
||||
backgroundFetchStarted = true;
|
||||
// BackgroundFetch event handler.
|
||||
const onEvent = async (taskId: string) => {
|
||||
console.log("[BackgroundFetch] task: ", taskId, AppState.currentState);
|
||||
// Do your background work...
|
||||
await onBackgroundSyncStarted();
|
||||
// IMPORTANT: You must signal to the OS that your task is complete.
|
||||
BackgroundFetch.finish(taskId);
|
||||
};
|
||||
|
||||
// Timeout callback is executed when your Task has exceeded its allowed running-time.
|
||||
// You must stop what you're doing immediately BackgroundFetch.finish(taskId)
|
||||
const onTimeout = async (taskId: string) => {
|
||||
console.warn("[BackgroundFetch] TIMEOUT: ", taskId);
|
||||
BackgroundFetch.finish(taskId);
|
||||
};
|
||||
|
||||
// Initialize BackgroundFetch only once when component mounts.
|
||||
const status = await BackgroundFetch.configure(
|
||||
{
|
||||
minimumFetchInterval: 15,
|
||||
enableHeadless: true,
|
||||
startOnBoot: true,
|
||||
stopOnTerminate: false,
|
||||
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
|
||||
},
|
||||
onEvent,
|
||||
onTimeout
|
||||
);
|
||||
DatabaseLogger.info(`[BackgroundFetch] configure status: ${status}`);
|
||||
console.log(`[BackgroundFetch] configure status: ${status}`);
|
||||
}
|
||||
|
||||
const task = async (event: { taskId: string; timeout: boolean }) => {
|
||||
// Get task id from event {}:
|
||||
const taskId = event.taskId;
|
||||
const isTimeout = event.timeout; // <-- true when your background-time has expired.
|
||||
if (isTimeout) {
|
||||
console.log("[BackgroundFetch] Headless TIMEOUT:", taskId);
|
||||
BackgroundFetch.finish(taskId);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
"[BackgroundFetch HeadlessTask] start: ",
|
||||
taskId,
|
||||
AppState.currentState
|
||||
);
|
||||
await onBackgroundSyncStarted();
|
||||
BackgroundFetch.finish(taskId);
|
||||
};
|
||||
|
||||
BackgroundFetch.registerHeadlessTask(task);
|
||||
|
||||
async function onBackgroundSyncStarted() {
|
||||
try {
|
||||
console.log("Background Sync", "start");
|
||||
await db.init();
|
||||
const user = await db.user?.getUser();
|
||||
if (user) {
|
||||
await db.sync(true, false);
|
||||
}
|
||||
await Notifications.setupReminders();
|
||||
console.log("Background Sync", "end");
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
console.log("Background Sync Error", (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const onBoot = async () => {
|
||||
try {
|
||||
console.log("BOOT TASK STARTED");
|
||||
await db.init();
|
||||
await Notifications.setupReminders();
|
||||
SettingsService.init();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(false);
|
||||
}
|
||||
console.log("BOOT TASK COMPLETE");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
AppRegistry.registerHeadlessTask(
|
||||
"com.streetwriters.notesnook.BOOT_TASK",
|
||||
() => {
|
||||
return onBoot;
|
||||
}
|
||||
);
|
||||
|
||||
export const BackgroundSync = {
|
||||
start
|
||||
};
|
||||
@@ -97,6 +97,7 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
return;
|
||||
}
|
||||
if (type === EventType.PRESS) {
|
||||
notifee.decrementBadgeCount();
|
||||
if (notification?.data?.type === "quickNote") return;
|
||||
editorState().movedAway = false;
|
||||
MMKV.removeItem("appState");
|
||||
@@ -125,8 +126,11 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
}
|
||||
|
||||
if (type === EventType.ACTION_PRESS) {
|
||||
notifee.decrementBadgeCount();
|
||||
switch (pressAction?.id) {
|
||||
case "REMINDER_SNOOZE": {
|
||||
await db.init();
|
||||
await db.notes?.init();
|
||||
const reminder = db.reminders?.reminder(
|
||||
notification?.id?.split("_")[0]
|
||||
);
|
||||
@@ -145,6 +149,8 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
break;
|
||||
}
|
||||
case "REMINDER_DISABLE": {
|
||||
await db.init();
|
||||
await db.notes?.init();
|
||||
const reminder = db.reminders?.reminder(
|
||||
notification?.id?.split("_")[0]
|
||||
);
|
||||
@@ -160,6 +166,8 @@ const onEvent = async ({ type, detail }: Event) => {
|
||||
break;
|
||||
}
|
||||
case "UNPIN": {
|
||||
await db.init();
|
||||
await db.notes?.init();
|
||||
remove(notification?.id as string);
|
||||
const reminder = db.reminders?.reminder(
|
||||
notification?.id?.split("_")[0]
|
||||
@@ -254,21 +262,25 @@ async function scheduleNotification(
|
||||
|
||||
try {
|
||||
const { title, description, priority } = reminder;
|
||||
|
||||
await clearAllPendingTriggersForId(reminder.id);
|
||||
await remove(reminder.id);
|
||||
if (reminder.disabled) return;
|
||||
if (reminder.disabled) {
|
||||
remove(reminder.id);
|
||||
return;
|
||||
}
|
||||
const triggers = await getTriggers(reminder);
|
||||
console.log(triggers);
|
||||
if (!triggers && reminder.mode === "permanent") {
|
||||
displayNotification({
|
||||
id: reminder.id,
|
||||
title: title,
|
||||
message: description || "",
|
||||
ongoing: true,
|
||||
subtitle: description || "",
|
||||
actions: ["UNPIN"]
|
||||
});
|
||||
if (reminder.mode === "permanent") {
|
||||
const notifications = await get();
|
||||
const pinned = notifications.findIndex((i) => i.id === reminder.id) > -1;
|
||||
if (!pinned) {
|
||||
displayNotification({
|
||||
id: reminder.id,
|
||||
title: title,
|
||||
message: description || "",
|
||||
ongoing: true,
|
||||
subtitle: description || "",
|
||||
actions: ["UNPIN"]
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
await setupIOSCategories();
|
||||
@@ -320,6 +332,7 @@ async function scheduleNotification(
|
||||
id: "default",
|
||||
mainComponent: "notesnook"
|
||||
},
|
||||
badgeCount: 1,
|
||||
actions: androidActions,
|
||||
sound: notificationSound?.url,
|
||||
style: !description
|
||||
@@ -332,6 +345,7 @@ async function scheduleNotification(
|
||||
ios: {
|
||||
interruptionLevel: "active",
|
||||
criticalVolume: 1.0,
|
||||
badgeCount: 1,
|
||||
critical:
|
||||
reminder.priority === "silent" || reminder.priority === "urgent"
|
||||
? false
|
||||
@@ -370,7 +384,8 @@ async function getChannelId(id: "silent" | "vibrate" | "urgent" | "default") {
|
||||
case "default":
|
||||
return await notifee.createChannel({
|
||||
id: "com.streetwriters.notesnook",
|
||||
name: "Default"
|
||||
name: "Default",
|
||||
vibration: false
|
||||
});
|
||||
case "silent":
|
||||
return await notifee.createChannel({
|
||||
@@ -419,6 +434,7 @@ async function displayNotification({
|
||||
id?: string;
|
||||
}) {
|
||||
if (!(await checkAndRequestPermissions())) return;
|
||||
|
||||
try {
|
||||
await notifee.displayNotification({
|
||||
id: id,
|
||||
@@ -430,6 +446,7 @@ async function displayNotification({
|
||||
},
|
||||
android: {
|
||||
ongoing: ongoing,
|
||||
smallIcon: "ic_stat_name",
|
||||
localOnly: true,
|
||||
channelId: await getChannelId("default"),
|
||||
autoCancel: false,
|
||||
@@ -733,6 +750,9 @@ async function setupReminders(checkNeedsScheduling = false) {
|
||||
const triggers = await notifee.getTriggerNotifications();
|
||||
|
||||
for (const reminder of reminders) {
|
||||
if (reminder.mode === "permanent") {
|
||||
await scheduleNotification(reminder);
|
||||
}
|
||||
const pending = triggers.filter((t) =>
|
||||
t.notification.id?.startsWith(reminder.id)
|
||||
);
|
||||
|
||||
@@ -160,7 +160,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 2023
|
||||
versionCode 2024
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -54,4 +54,7 @@
|
||||
|
||||
# Wix
|
||||
-keep class org.apache.commons.lang3.** { *; }
|
||||
-keep class org.apache.commons.io.** { *; }
|
||||
-keep class org.apache.commons.io.** { *; }
|
||||
|
||||
# Background fetch
|
||||
-keep class com.transistorsoft.rnbackgroundfetch.HeadlessTask { *; }
|
||||
@@ -130,6 +130,8 @@
|
||||
android:stopWithTask="false" />
|
||||
<service android:name="com.asterinet.react.bgactions.RNBackgroundActionsTask" />
|
||||
|
||||
<service android:name="com.streetwriters.notesnook.BootTaskService" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@@ -150,22 +152,13 @@
|
||||
android:resource="@xml/file_viewer_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- Change the value to true to enable pop-up for in foreground on receiving remote notifications (for prevent duplicating while showing local notifications set this to false) -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
|
||||
android:value="false"/>
|
||||
<!-- Change the resource name to your App's accent color - or any other color you want -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||
android:resource="@android:color/white"/> <!-- or @android:color/{name} to use a standard color -->
|
||||
|
||||
<receiver android:exported="true" android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||
<receiver android:exported="true" android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
||||
<receiver android:exported="true" android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
|
||||
<receiver android:exported="true" android:name=".BootRecieverService" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.app.ActivityManager;
|
||||
import com.facebook.react.HeadlessJsTaskService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BootRecieverService extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!isAppOnForeground((context))) {
|
||||
/**
|
||||
We will start our service and send extra info about
|
||||
network connections
|
||||
**/
|
||||
Intent serviceIntent = new Intent(context, BootTaskService.class);
|
||||
context.startService(serviceIntent);
|
||||
HeadlessJsTaskService.acquireWakeLockNow(context);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAppOnForeground(Context context) {
|
||||
/**
|
||||
We need to check if app is in foreground otherwise the app will crash.
|
||||
http://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
|
||||
**/
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
List<ActivityManager.RunningAppProcessInfo> appProcesses =
|
||||
activityManager.getRunningAppProcesses();
|
||||
if (appProcesses == null) {
|
||||
return false;
|
||||
}
|
||||
final String packageName = context.getPackageName();
|
||||
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
|
||||
if (appProcess.importance ==
|
||||
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
||||
appProcess.processName.equals(packageName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.streetwriters.notesnook;
|
||||
|
||||
import android.content.Intent;
|
||||
import com.facebook.react.HeadlessJsTaskService;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class BootTaskService extends HeadlessJsTaskService {
|
||||
|
||||
@Override
|
||||
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
|
||||
return new HeadlessJsTaskConfig(
|
||||
"com.streetwriters.notesnook.BOOT_TASK",
|
||||
Arguments.createMap(),
|
||||
30000, // timeout for the task
|
||||
false // optional: defines whether or not the task is allowed in foreground. Default is false
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -69,5 +69,8 @@ allprojects {
|
||||
|
||||
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
maven {
|
||||
url("${project(':react-native-background-fetch').projectDir}/libs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
- Cross platform Reminders 🔔
|
||||
- Multi-factor auth is enabled by default for all users to improve login security
|
||||
- Now you can change account email from Settings -> Account Settings -> Change email
|
||||
- Added options to disable sync, auto-sync & real-time sync from settings.
|
||||
- Improved PDF exports styling
|
||||
- Many bug fixes and improvements
|
||||
- Added background sync support
|
||||
- Fix new features sheet showing on every launch
|
||||
- Fix permanent reminders removed on rebooting device
|
||||
- Fixed unable to unpin/disable reminders from notifications
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -8,6 +8,8 @@ import Config from 'react-native-config';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import appJson from './app.json';
|
||||
import Notifications from '../app/services/notifications';
|
||||
import {BackgroundSync} from '../app/services/background-sync';
|
||||
|
||||
const appName = appJson.name;
|
||||
if (Config.isTesting) {
|
||||
Date.prototype.toLocaleString = () => 'XX-XX-XX';
|
||||
@@ -20,6 +22,7 @@ if (__DEV__) {
|
||||
LogBox.ignoreAllLogs();
|
||||
}
|
||||
let NotesnookShare;
|
||||
BackgroundSync.start();
|
||||
Notifications.init();
|
||||
let QuickNoteIOS;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
65AA857E25E6DDEC00772A01 /* NotesWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65AA857D25E6DDEC00772A01 /* NotesWidget.swift */; };
|
||||
65AA858025E6DDEE00772A01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 65AA857F25E6DDEE00772A01 /* Assets.xcassets */; };
|
||||
65AA858425E6DDEE00772A01 /* NotesWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 65AA857725E6DDEC00772A01 /* NotesWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
65B450AD2976AF0C00EA090B /* RNBackgroundFetch+AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B450AC2976AF0B00EA090B /* RNBackgroundFetch+AppDelegate.m */; };
|
||||
65B5014425A672B200E2D264 /* ShareViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65B5014325A672B200E2D264 /* ShareViewController.m */; };
|
||||
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 65B5014525A672B200E2D264 /* MainInterface.storyboard */; };
|
||||
65B5014B25A672B200E2D264 /* Make Note.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 65B5014025A672B200E2D264 /* Make Note.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -122,6 +123,7 @@
|
||||
65AA857D25E6DDEC00772A01 /* NotesWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesWidget.swift; sourceTree = "<group>"; };
|
||||
65AA857F25E6DDEE00772A01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
65AA858125E6DDEE00772A01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
65B450AC2976AF0B00EA090B /* RNBackgroundFetch+AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RNBackgroundFetch+AppDelegate.m"; path = "../../node_modules/react-native-background-fetch/ios/RNBackgroundFetch/RNBackgroundFetch+AppDelegate.m"; sourceTree = "<group>"; };
|
||||
65B5014025A672B200E2D264 /* Make Note.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Make Note.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
65B5014325A672B200E2D264 /* ShareViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareViewController.m; sourceTree = "<group>"; };
|
||||
65B5014625A672B200E2D264 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
|
||||
@@ -221,6 +223,7 @@
|
||||
13B07FAE1A68108700A75B9A /* Notesnook */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
65B450AC2976AF0B00EA090B /* RNBackgroundFetch+AppDelegate.m */,
|
||||
6593E4A2281C345400492C50 /* AppDelegate.mm */,
|
||||
6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */,
|
||||
659BE46625E11A5100E05671 /* notesnook-text.png */,
|
||||
@@ -941,6 +944,7 @@
|
||||
65E0340B257B9FF100793428 /* File.swift in Sources */,
|
||||
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
65B450AD2976AF0C00EA090B /* RNBackgroundFetch+AppDelegate.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1083,7 +1087,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2023;
|
||||
CURRENT_PROJECT_VERSION = 2024;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1157,7 +1161,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1187,7 +1191,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Notesnook/Notesnook.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2023;
|
||||
CURRENT_PROJECT_VERSION = 2024;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
GCC_GENERATE_DEBUGGING_SYMBOLS = YES;
|
||||
@@ -1260,7 +1264,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1418,7 +1422,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2023;
|
||||
CURRENT_PROJECT_VERSION = 2024;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1430,7 +1434,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1460,7 +1464,7 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2023;
|
||||
CURRENT_PROJECT_VERSION = 2024;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1472,7 +1476,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1501,7 +1505,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2023;
|
||||
CURRENT_PROJECT_VERSION = 2024;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1575,7 +1579,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1605,7 +1609,7 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2023;
|
||||
CURRENT_PROJECT_VERSION = 2024;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1679,7 +1683,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.4.0;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#import <React/RCTAppSetupUtils.h>
|
||||
#import <React/RCTLinkingManager.h>
|
||||
#import "RNShortcuts.h"
|
||||
#import <TSBackgroundFetch/TSBackgroundFetch.h>
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
#import <React/CoreModulesPlugins.h>
|
||||
@@ -62,6 +63,7 @@ RCTBridge *bridge;
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
self.window.rootViewController = rootViewController;
|
||||
[self.window makeKeyAndVisible];
|
||||
[[TSBackgroundFetch sharedInstance] didFinishLaunching];
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<dict>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>com.transistorsoft.fetch</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
||||
@@ -345,6 +345,8 @@ PODS:
|
||||
- React-Core
|
||||
- rn-fetch-blob (0.12.0):
|
||||
- React-Core
|
||||
- RNBackgroundFetch (4.1.7):
|
||||
- React-Core
|
||||
- RNBootSplash (4.3.2):
|
||||
- React-Core
|
||||
- RNCCheckbox (0.5.12):
|
||||
@@ -481,6 +483,7 @@ DEPENDENCIES:
|
||||
- ReactCommon/turbomodule/core (from `../../node_modules/react-native/ReactCommon`)
|
||||
- rn-extensions-share (from `../../node_modules/rn-extensions-share`)
|
||||
- rn-fetch-blob (from `../../node_modules/rn-fetch-blob`)
|
||||
- RNBackgroundFetch (from `../../node_modules/react-native-background-fetch`)
|
||||
- RNBootSplash (from `../../node_modules/react-native-bootsplash`)
|
||||
- "RNCCheckbox (from `../../node_modules/@react-native-community/checkbox`)"
|
||||
- "RNCClipboard (from `../../node_modules/@react-native-clipboard/clipboard`)"
|
||||
@@ -622,6 +625,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/rn-extensions-share"
|
||||
rn-fetch-blob:
|
||||
:path: "../../node_modules/rn-fetch-blob"
|
||||
RNBackgroundFetch:
|
||||
:path: "../../node_modules/react-native-background-fetch"
|
||||
RNBootSplash:
|
||||
:path: "../../node_modules/react-native-bootsplash"
|
||||
RNCCheckbox:
|
||||
@@ -736,6 +741,7 @@ SPEC CHECKSUMS:
|
||||
ReactCommon: 1e783348b9aa73ae68236271df972ba898560a95
|
||||
rn-extensions-share: 3f0ecce20dfbca1f0358deb4ebfb9ee121a6d92a
|
||||
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
|
||||
RNBackgroundFetch: 13fbe7b23d4a082eac8caa7b08c5bcbeabd92f79
|
||||
RNBootSplash: 5f346163977573d6b2aeba1b25df9d2245c0d73c
|
||||
RNCCheckbox: ed1b4ca295475b41e7251ebae046360a703b6eb5
|
||||
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
|
||||
|
||||
@@ -56,7 +56,8 @@
|
||||
"react-native-modal-datetime-picker":"14.0.0",
|
||||
"@react-native-community/datetimepicker":"6.6.0",
|
||||
"react-native-date-picker": "4.2.6",
|
||||
"react-native-notification-sounds": "0.5.5"
|
||||
"react-native-notification-sounds": "0.5.5",
|
||||
"react-native-background-fetch": "4.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
- Cross platform Reminders 🔔
|
||||
- Multi-factor auth is enabled by default for all users to improve login security
|
||||
- Now you can change account email from Settings -> Account Settings -> Change email
|
||||
- Added options to disable sync, auto-sync & real-time sync from settings.
|
||||
- Improved PDF exports styling
|
||||
- Many bug fixes and improvements
|
||||
- Added background sync support
|
||||
- Fix new features sheet showing on every launch
|
||||
- Fix permanent reminders removed on rebooting device
|
||||
- Fixed unable to unpin/disable reminders from notifications
|
||||
|
||||
Thank you for using Notesnook!
|
||||
16
apps/mobile/package-lock.json
generated
16
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
"native/",
|
||||
@@ -78,6 +78,7 @@
|
||||
"react": "18.0.0",
|
||||
"react-native": "0.69.7",
|
||||
"react-native-background-actions": "^2.6.6",
|
||||
"react-native-background-fetch": "4.1.7",
|
||||
"react-native-begin-background-task": "https://github.com/blockfirm/react-native-begin-background-task.git",
|
||||
"react-native-bootsplash": "^4.1.4",
|
||||
"react-native-config": "^1.4.6",
|
||||
@@ -17976,6 +17977,11 @@
|
||||
"react-native": ">=0.47.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-background-fetch": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-native-background-fetch/-/react-native-background-fetch-4.1.7.tgz",
|
||||
"integrity": "sha512-dFY/AOZbEH+ldwZvq/UhhWI5+dyBYL1Xo5wqy6u5DLH+ef8jLIrwKQRmfLAWiP5BtZe6ktA55HM19X39qHoKEQ=="
|
||||
},
|
||||
"node_modules/react-native-begin-background-task": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "git+ssh://git@github.com/blockfirm/react-native-begin-background-task.git#c2aa793249db6cc6298a812905f955a99b864e78",
|
||||
@@ -24359,6 +24365,7 @@
|
||||
"react-native": "0.69.7",
|
||||
"react-native-actions-shortcuts": "^1.0.1",
|
||||
"react-native-background-actions": "^2.6.6",
|
||||
"react-native-background-fetch": "4.1.7",
|
||||
"react-native-begin-background-task": "https://github.com/blockfirm/react-native-begin-background-task.git",
|
||||
"react-native-bootsplash": "^4.1.4",
|
||||
"react-native-bundle-visualizer": "^3.1.1",
|
||||
@@ -34852,6 +34859,11 @@
|
||||
"eventemitter3": "^4.0.7"
|
||||
}
|
||||
},
|
||||
"react-native-background-fetch": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-native-background-fetch/-/react-native-background-fetch-4.1.7.tgz",
|
||||
"integrity": "sha512-dFY/AOZbEH+ldwZvq/UhhWI5+dyBYL1Xo5wqy6u5DLH+ef8jLIrwKQRmfLAWiP5BtZe6ktA55HM19X39qHoKEQ=="
|
||||
},
|
||||
"react-native-begin-background-task": {
|
||||
"version": "git+ssh://git@github.com/blockfirm/react-native-begin-background-task.git#c2aa793249db6cc6298a812905f955a99b864e78",
|
||||
"from": "react-native-begin-background-task@https://github.com/blockfirm/react-native-begin-background-task.git"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
@@ -57,13 +57,12 @@ export class NavigationMenuModel {
|
||||
}
|
||||
|
||||
class NavigationItemModel {
|
||||
private readonly title: Locator;
|
||||
// private readonly title: Locator;
|
||||
private readonly shortcut: Locator;
|
||||
private readonly menu: ContextMenuModel;
|
||||
private readonly page: Page;
|
||||
constructor(private readonly locator: Locator) {
|
||||
this.page = locator.page();
|
||||
this.title = locator.locator(getTestId("title"));
|
||||
this.shortcut = locator.locator(getTestId("shortcut"));
|
||||
this.menu = new ContextMenuModel(this.page);
|
||||
}
|
||||
@@ -77,7 +76,7 @@ class NavigationItemModel {
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return await this.title.textContent();
|
||||
return await this.locator.getAttribute("title");
|
||||
}
|
||||
|
||||
async renameColor(alias: string) {
|
||||
|
||||
@@ -38,6 +38,10 @@ export class RemindersViewModel extends BaseViewModel {
|
||||
await this.createButton.click();
|
||||
|
||||
await fillReminderDialog(this.page, reminder);
|
||||
}
|
||||
|
||||
async createReminderAndWait(reminder: Partial<Reminder>) {
|
||||
await this.createReminder(reminder);
|
||||
|
||||
if (reminder.title) await this.waitForItem(reminder.title);
|
||||
return await this.findReminder(reminder);
|
||||
|
||||
@@ -36,7 +36,7 @@ test("add a one-time reminder", async ({ page }, info) => {
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
|
||||
const reminder = await reminders.createReminder(ONE_TIME_REMINDER);
|
||||
const reminder = await reminders.createReminderAndWait(ONE_TIME_REMINDER);
|
||||
|
||||
expect(reminder).toBeDefined();
|
||||
|
||||
@@ -44,13 +44,34 @@ test("add a one-time reminder", async ({ page }, info) => {
|
||||
await page.waitForEvent("dialog");
|
||||
});
|
||||
|
||||
test("adding a one-time reminder before current time should not be possible", async ({
|
||||
page
|
||||
}, info) => {
|
||||
info.setTimeout(90 * 1000);
|
||||
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
|
||||
await reminders.createReminder({
|
||||
...ONE_TIME_REMINDER,
|
||||
date: 0
|
||||
});
|
||||
|
||||
expect(
|
||||
await app.toasts.waitForToast(
|
||||
"Reminder time cannot be earlier than the current time."
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
for (const recurringMode of ["Daily", "Weekly", "Monthly"] as const) {
|
||||
test(`add a recurring reminder (${recurringMode})`, async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
|
||||
const reminder = await reminders.createReminder({
|
||||
const reminder = await reminders.createReminderAndWait({
|
||||
...ONE_TIME_REMINDER,
|
||||
mode: "repeat",
|
||||
recurringMode:
|
||||
@@ -67,11 +88,26 @@ for (const recurringMode of ["Daily", "Weekly", "Monthly"] as const) {
|
||||
});
|
||||
}
|
||||
|
||||
test(`add a recurring reminder before current time`, async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
|
||||
const reminder = await reminders.createReminderAndWait({
|
||||
...ONE_TIME_REMINDER,
|
||||
mode: "repeat",
|
||||
recurringMode: "day",
|
||||
date: 0
|
||||
});
|
||||
|
||||
expect(reminder).toBeDefined();
|
||||
});
|
||||
|
||||
test("delete a reminder", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
const reminder = await reminders.createReminder(ONE_TIME_REMINDER);
|
||||
const reminder = await reminders.createReminderAndWait(ONE_TIME_REMINDER);
|
||||
|
||||
await reminder?.delete();
|
||||
|
||||
@@ -89,7 +125,7 @@ test("edit a reminder", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
const reminder = await reminders.createReminder(ONE_TIME_REMINDER);
|
||||
const reminder = await reminders.createReminderAndWait(ONE_TIME_REMINDER);
|
||||
const beforeReminderTime = await reminder?.getReminderTime();
|
||||
const beforeDescription = await reminder?.getDescription();
|
||||
const beforeTitle = await reminder?.getTitle();
|
||||
@@ -109,7 +145,7 @@ test("disable a reminder", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
const reminder = await reminders.createReminder(ONE_TIME_REMINDER);
|
||||
const reminder = await reminders.createReminderAndWait(ONE_TIME_REMINDER);
|
||||
|
||||
await reminder?.toggle();
|
||||
|
||||
@@ -122,7 +158,7 @@ test("enable a disabled reminder", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const reminders = await app.goToReminders();
|
||||
const reminder = await reminders.createReminder(ONE_TIME_REMINDER);
|
||||
const reminder = await reminders.createReminderAndWait(ONE_TIME_REMINDER);
|
||||
await reminder?.toggle();
|
||||
|
||||
await reminder?.toggle();
|
||||
|
||||
@@ -45,6 +45,10 @@ if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(app.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {BrowserWindow}
|
||||
*/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
!function(){"use strict";var t=function(t,e,n){var a=t[e];return function(){for(var e=[],i=arguments.length;i--;)e[i]=arguments[i];return n.apply(null,e),a.apply(t,e)}},e=function(){var t=window.doNotTrack,e=window.navigator,n=window.external,a=t||e.doNotTrack||e.msDoNotTrack||n&&"function"==typeof n.msTrackingProtectionEnabled&&n.msTrackingProtectionEnabled();return!0===a||1===a||"yes"===a||"1"===a};!function(n){var a=n.screen,i=a.width,r=a.height,o=n.navigator.language,c=n.location,s=c.hostname,u=c.pathname,l=c.search,d=n.localStorage,f=n.sessionStorage,v=n.document,p=n.history,m=v.querySelector("script[data-website-id]");if(m){var h,g=function(t){return m&&m.getAttribute(t)},w=g("data-website-id"),y=g("data-host-url"),S="false"!==g("data-auto-track"),E=g("data-do-not-track"),k=g("data-cache"),b=g("data-domains"),T=d.getItem("umami.disabled")||E&&e()||b&&!b.split(",").map((function(t){return t.trim()})).includes(s),N=y?(h=y)&&h.length>1&&h.endsWith("/")?h.slice(0,-1):h:m.src.split("/").slice(0,-1).join("/"),j=i+"x"+r,q=[],I=""+u+l,L=v.referrer,O=function(t,e,n){if(!T){var a="umami.cache",i={website:n,hostname:s,screen:j,language:o,cache:k&&f.getItem(a)};e&&Object.keys(e).forEach((function(t){i[t]=e[t]})),function(t,e,n){var a=new XMLHttpRequest;a.open("POST",t,!0),a.setRequestHeader("Content-Type","application/json"),a.onreadystatechange=function(){4===a.readyState&&n&&n(a.response)},a.send(JSON.stringify(e))}(N+"/api/collect",{type:t,payload:i},(function(t){return k&&f.setItem(a,t)}))}},P=function(t,e,n){return void 0===t&&(t=I),void 0===e&&(e=L),void 0===n&&(n=w),O("pageview",{url:t,referrer:e},n)},x=function(t,e,n,a){return void 0===e&&(e="custom"),void 0===n&&(n=I),void 0===a&&(a=w),O("event",{event_type:e,event_value:t,url:n},a)},A=function(){v.querySelectorAll("[class*='umami--']").forEach((function(t){t.className.split(" ").forEach((function(e){if(/^umami--([a-z]+)--([\w]+[\w-]*)$/.test(e)){var n=e.split("--"),a=n[1],i=n[2],r=function(){return x(i,a)};q.push([t,a,r]),t.addEventListener(a,r,!0)}}))}))},H=function(){q.forEach((function(t){var e=t[0],n=t[1],a=t[2];e&&e.removeEventListener(n,a,!0)})),q.length=0},R=function(t,e,n){if(n){H(),L=I;var a=n.toString();(I="http"===a.substring(0,4)?"/"+a.split("/").splice(3).join("/"):a)!==L&&P(I,L),setTimeout(A,300)}};if(!n.umami){var _=function(t){return x(t)};_.trackView=P,_.trackEvent=x,_.addEvents=A,_.removeEvents=H,n.umami=_}S&&!T&&(p.pushState=t(p,"pushState",R),p.replaceState=t(p,"replaceState",R),P(I,L),A())}}(window)}();
|
||||
@@ -298,7 +298,7 @@ function CalltoAction({ action, variant, sx, removeAnnouncement }) {
|
||||
onClick={async () => {
|
||||
if (removeAnnouncement) removeAnnouncement();
|
||||
closeOpenedDialog();
|
||||
trackEvent(ANALYTICS_EVENTS.announcementCta, action.data);
|
||||
trackEvent(ANALYTICS_EVENTS.announcementCta, action);
|
||||
switch (action.type) {
|
||||
case "link": {
|
||||
const url = new URL(action.data);
|
||||
|
||||
@@ -51,10 +51,7 @@ function Announcements({ announcements, removeAnnouncement }) {
|
||||
}}
|
||||
title="Dismiss announcement"
|
||||
onClick={() => {
|
||||
trackEvent(
|
||||
ANALYTICS_EVENTS.announcementDismissed,
|
||||
announcement.title
|
||||
);
|
||||
trackEvent(ANALYTICS_EVENTS.announcementDismissed, announcement);
|
||||
removeAnnouncement && removeAnnouncement(announcement.id);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function AddReminderDialog(props: AddReminderDialogProps) {
|
||||
|
||||
const dateTime = dayjs(getDateTime(date, time));
|
||||
|
||||
if (dateTime.isBefore(dayjs())) {
|
||||
if (mode !== Modes.REPEAT && dateTime.isBefore(dayjs())) {
|
||||
showToast(
|
||||
"error",
|
||||
"Reminder time cannot be earlier than the current time."
|
||||
|
||||
@@ -41,6 +41,7 @@ import { useCallback, useState } from "react";
|
||||
import Config from "../../utils/config";
|
||||
import { getAllAccents } from "@notesnook/theme";
|
||||
import { isMacStoreApp } from "../../utils/platform";
|
||||
import { doNotTrack } from "../../utils/telemetry";
|
||||
|
||||
const newUserSteps = [
|
||||
{
|
||||
@@ -48,7 +49,7 @@ const newUserSteps = [
|
||||
subtitle: "Write with freedom. Never compromise on privacy again.",
|
||||
buttonText: "Get started",
|
||||
image: <Note width={120} />,
|
||||
component: TrackingConsent
|
||||
component: doNotTrack() ? null : TrackingConsent
|
||||
},
|
||||
{
|
||||
title: "Choose your style",
|
||||
|
||||
@@ -53,7 +53,7 @@ type Route = {
|
||||
title: string;
|
||||
path: string;
|
||||
icon: Icon;
|
||||
isNew?: boolean;
|
||||
tag?: string;
|
||||
};
|
||||
|
||||
const navigationHistory = new Map();
|
||||
@@ -92,7 +92,8 @@ const routes: Route[] = [
|
||||
{
|
||||
title: "Reminders",
|
||||
path: "/reminders",
|
||||
icon: Reminders
|
||||
icon: Reminders,
|
||||
tag: "Beta"
|
||||
},
|
||||
{ title: "Trash", path: "/trash", icon: Trash }
|
||||
];
|
||||
@@ -169,7 +170,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
key={item.path}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
isNew={item.isNew}
|
||||
tag={item.tag}
|
||||
selected={
|
||||
item.path === "/"
|
||||
? location === item.path
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useMenuTrigger } from "../../hooks/use-menu";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
import { Icon, Circle, Shortcut } from "../icons";
|
||||
import { Icon, Shortcut } from "../icons";
|
||||
|
||||
type NavigationItemProps = {
|
||||
icon: Icon;
|
||||
@@ -32,7 +32,7 @@ type NavigationItemProps = {
|
||||
isTablet?: boolean;
|
||||
isLoading?: boolean;
|
||||
isShortcut?: boolean;
|
||||
isNew?: boolean;
|
||||
tag?: string;
|
||||
selected?: boolean;
|
||||
onClick?: () => void;
|
||||
count?: number;
|
||||
@@ -47,7 +47,7 @@ function NavigationItem(props: PropsWithChildren<NavigationItemProps>) {
|
||||
title,
|
||||
isLoading,
|
||||
isShortcut,
|
||||
isNew,
|
||||
tag,
|
||||
children,
|
||||
isTablet,
|
||||
selected,
|
||||
@@ -102,13 +102,6 @@ function NavigationItem(props: PropsWithChildren<NavigationItemProps>) {
|
||||
color={color || (selected ? "primary" : "icon")}
|
||||
rotate={isLoading}
|
||||
/>
|
||||
{isNew && (
|
||||
<Circle
|
||||
size={6}
|
||||
sx={{ position: "absolute", bottom: "8px", left: "23px" }}
|
||||
color={"primary"}
|
||||
/>
|
||||
)}
|
||||
{isShortcut && (
|
||||
<Shortcut
|
||||
size={8}
|
||||
@@ -132,6 +125,21 @@ function NavigationItem(props: PropsWithChildren<NavigationItemProps>) {
|
||||
data-test-id="title"
|
||||
>
|
||||
{title}
|
||||
{tag && (
|
||||
<Text
|
||||
variant="subBody"
|
||||
as="span"
|
||||
sx={{
|
||||
bg: "primary",
|
||||
color: "static",
|
||||
ml: 1,
|
||||
px: "small",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Button>
|
||||
{children ? (
|
||||
|
||||
@@ -33,7 +33,8 @@ function Toggle(props) {
|
||||
onToggled,
|
||||
onlyIf,
|
||||
premium,
|
||||
testId
|
||||
testId,
|
||||
disabled
|
||||
} = props;
|
||||
const onClick = useCallback(async () => {
|
||||
if (isUserPremium() || !premium || isToggled) onToggled();
|
||||
@@ -45,10 +46,11 @@ function Toggle(props) {
|
||||
if (onlyIf === false) return null;
|
||||
return (
|
||||
<Flex
|
||||
onClick={onClick}
|
||||
onClick={disabled ? null : onClick}
|
||||
data-test-id={testId}
|
||||
py={2}
|
||||
sx={{
|
||||
opacity: disabled ? 0.7 : 1,
|
||||
cursor: "pointer",
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: "border",
|
||||
@@ -58,7 +60,7 @@ function Toggle(props) {
|
||||
}}
|
||||
>
|
||||
<Tip text={title} tip={isToggled ? onTip : offTip} sx={{ mr: 2 }} />
|
||||
<Switch onClick={onClick} checked={isToggled} />
|
||||
<Switch onClick={disabled ? null : onClick} checked={isToggled} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { initializeDatabase } from "../common/db";
|
||||
import { loadTrackerScript } from "../utils/analytics";
|
||||
import "../utils/analytics";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
loadTrackerScript();
|
||||
console.log = () => {};
|
||||
}
|
||||
|
||||
|
||||
35
apps/web/src/hooks/use-telemetry.ts
Normal file
35
apps/web/src/hooks/use-telemetry.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 {
|
||||
doNotTrack,
|
||||
isTelemetryEnabled,
|
||||
setTelemetry
|
||||
} from "../utils/telemetry";
|
||||
|
||||
export function useTelemetry() {
|
||||
const [enabled, setEnabled] = useState(isTelemetryEnabled());
|
||||
|
||||
useEffect(() => {
|
||||
setTelemetry(enabled);
|
||||
}, [enabled]);
|
||||
|
||||
return [enabled, setEnabled, doNotTrack()] as const;
|
||||
}
|
||||
@@ -17,42 +17,21 @@ 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 Config from "./config";
|
||||
import { getPlatform } from "./platform";
|
||||
import { isTelemetryEnabled } from "./telemetry";
|
||||
import { appVersion } from "./version";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
umami?: {
|
||||
trackEvent: (
|
||||
value: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
websiteId?: string
|
||||
) => void;
|
||||
trackView: (url: string, referrer?: string, websiteId?: string) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
type PageView = {
|
||||
type: "pageview";
|
||||
referrer: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export function loadTrackerScript() {
|
||||
if (Config.get("telemetry") === "false") return;
|
||||
const script = document.createElement("script");
|
||||
script.src = "/an.js";
|
||||
script.async = true;
|
||||
script.dataset.websiteId = "f16c07d9-c77b-4781-bfbd-f58e95640002";
|
||||
|
||||
if (process.env.REACT_APP_PLATFORM !== "desktop")
|
||||
script.dataset.domains = "app.notesnook.com";
|
||||
script.dataset.autoTrack = "false";
|
||||
script.dataset.doNotTrack = "true";
|
||||
script.dataset.hostUrl = "https://analytics.streetwriters.co";
|
||||
const firstScriptElement = document.getElementsByTagName("script")[0];
|
||||
script.onload = function () {
|
||||
trackVisit();
|
||||
};
|
||||
firstScriptElement.parentNode?.insertBefore(script, firstScriptElement);
|
||||
}
|
||||
type Event = {
|
||||
type: "event";
|
||||
event_name: string;
|
||||
event_data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type TrackerEvent = {
|
||||
name: string;
|
||||
@@ -104,22 +83,86 @@ export const ANALYTICS_EVENTS = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function trackEvent(event: TrackerEvent, eventMessage?: string) {
|
||||
if (Config.get("telemetry") === "false") return;
|
||||
if (!window.umami) return;
|
||||
const baseUrl = `https://analytics.streetwriters.co/api/collect`;
|
||||
|
||||
async function trackUmamiView(url: string, referrer?: string) {
|
||||
return collect({
|
||||
type: "pageview",
|
||||
referrer: referrer || window.document.referrer,
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
function trackUmamiEvent(name: string, data: Record<string, unknown>) {
|
||||
return collect({
|
||||
type: "event",
|
||||
event_name: name,
|
||||
event_data: data
|
||||
});
|
||||
}
|
||||
|
||||
async function collect(event: PageView | Event) {
|
||||
const {
|
||||
screen: { width, height },
|
||||
navigator: { language },
|
||||
location: { hostname, pathname, search }
|
||||
} = window;
|
||||
|
||||
const screen = `${width}x${height}`;
|
||||
const currentUrl =
|
||||
(event.type === "pageview" && event.url) || `${pathname}${search}`;
|
||||
|
||||
const body = {
|
||||
payload: {
|
||||
website: `f16c07d9-c77b-4781-bfbd-f58e95640002`,
|
||||
hostname,
|
||||
screen,
|
||||
language,
|
||||
url: currentUrl,
|
||||
...event
|
||||
},
|
||||
type: event.type
|
||||
};
|
||||
|
||||
try {
|
||||
await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
keepalive: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export function trackEvent(
|
||||
event: TrackerEvent,
|
||||
data?: Record<string, unknown>
|
||||
) {
|
||||
if (!isTelemetryEnabled()) return;
|
||||
if (event.type === "view") trackVisit(event.name);
|
||||
else if (eventMessage) window.umami.trackEvent(eventMessage, event.name);
|
||||
else if (data) trackUmamiEvent(event.name, data);
|
||||
}
|
||||
|
||||
export function trackVisit(url = "/") {
|
||||
if (Config.get("telemetry") === "false") return;
|
||||
const platform = getPlatform();
|
||||
if (!window.umami || !platform) return;
|
||||
if (!isTelemetryEnabled()) return;
|
||||
|
||||
window.umami.trackView(url, window.document.referrer);
|
||||
const platform = getPlatform();
|
||||
if (!platform) return;
|
||||
|
||||
trackUmamiView(window.document.referrer);
|
||||
if (url === "/")
|
||||
trackEvent(
|
||||
ANALYTICS_EVENTS.version,
|
||||
`${appVersion.formatted}-${platform.toLowerCase()}`
|
||||
);
|
||||
trackEvent(ANALYTICS_EVENTS.version, {
|
||||
version: appVersion.formatted,
|
||||
platform
|
||||
});
|
||||
}
|
||||
|
||||
if (isTelemetryEnabled()) {
|
||||
document.addEventListener("readystatechange", () => {
|
||||
trackVisit();
|
||||
});
|
||||
}
|
||||
|
||||
53
apps/web/src/utils/telemetry.ts
Normal file
53
apps/web/src/utils/telemetry.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 Config from "./config";
|
||||
|
||||
export function isTelemetryEnabled() {
|
||||
return (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
(Config.get("telemetry", doNotTrack()) || !doNotTrack())
|
||||
);
|
||||
}
|
||||
|
||||
export function setTelemetry(state: boolean) {
|
||||
Config.set("telemetry", state);
|
||||
}
|
||||
|
||||
export function doNotTrack() {
|
||||
const { navigator, external } = window;
|
||||
|
||||
const msTrackProtection = "msTrackingProtectionEnabled";
|
||||
const msTracking = () => {
|
||||
return (
|
||||
external &&
|
||||
msTrackProtection in external &&
|
||||
typeof external[msTrackProtection] === "function" &&
|
||||
external[msTrackProtection]()
|
||||
);
|
||||
};
|
||||
|
||||
const dnt =
|
||||
("doNotTrack" in window && window.doNotTrack) ||
|
||||
navigator.doNotTrack ||
|
||||
("msDoNotTrack" in navigator && navigator.doNotTrack) ||
|
||||
msTracking();
|
||||
|
||||
return dnt == "1" || dnt === "yes";
|
||||
}
|
||||
@@ -156,7 +156,7 @@ function BlogPromoBanner() {
|
||||
title={account.title}
|
||||
onClick={() => {
|
||||
window.open(account.link, "_blank");
|
||||
trackEvent(ANALYTICS_EVENTS.socialLink, account.title);
|
||||
trackEvent(ANALYTICS_EVENTS.socialLink, { link: account.link });
|
||||
}}
|
||||
size={30}
|
||||
sx={{ mr: 1, cursor: "pointer" }}
|
||||
|
||||
@@ -74,6 +74,7 @@ import { clearLogs, downloadLogs } from "../utils/logger";
|
||||
import { exportNotes } from "../common/export";
|
||||
import { scheduleBackups } from "../common/reminders";
|
||||
import usePrivacyMode from "../hooks/use-privacy-mode";
|
||||
import { useTelemetry } from "../hooks/use-telemetry";
|
||||
|
||||
function subscriptionStatusToString(user) {
|
||||
const status = user?.subscription?.type;
|
||||
@@ -188,10 +189,8 @@ function Settings() {
|
||||
"backupStorageLocation",
|
||||
PATHS.backupsDirectory
|
||||
);
|
||||
const [enableTelemetry, setEnableTelemetry] = usePersistentState(
|
||||
"telemetry",
|
||||
true
|
||||
);
|
||||
const [enableTelemetry, setEnableTelemetry, isTelemetryDisabled] =
|
||||
useTelemetry();
|
||||
const [privacyMode, setPrivacyMode] = usePrivacyMode();
|
||||
const [showReminderNotifications, setShowReminderNotifications] =
|
||||
usePersistentState("reminderNotifications", true);
|
||||
@@ -824,7 +823,7 @@ function Settings() {
|
||||
<Toggle
|
||||
title="Enable telemetry"
|
||||
onTip="Usage data & crash reports will be sent to us (no 3rd party involved) for analytics. All data is anonymous as mentioned in our privacy policy."
|
||||
offTip="Do not collect any data or crash reports"
|
||||
offTip={"Do not collect any data or crash reports"}
|
||||
onToggled={() => {
|
||||
setEnableTelemetry(!enableTelemetry);
|
||||
}}
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
- Multi-factor auth is enabled by default for all users to improve login security
|
||||
- Now you can change account email from Settings -> Account Settings -> Change email
|
||||
- Added options to disable sync, auto-sync & real-time sync from settings.
|
||||
- Improved PDF exports styling
|
||||
- Improved PDF exports styling- Added background sync support
|
||||
- Fix new features sheet showing on every launch
|
||||
- Fix permanent reminders removed on rebooting device
|
||||
- Fixed unable to unpin/disable reminders from notifications
|
||||
|
||||
Thank you for using Notesnook!
|
||||
- Many bug fixes and improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
6
fastlane/metadata/android/en-US/changelogs/4196328.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/4196328.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
- Added background sync support
|
||||
- Fix new features sheet showing on every launch
|
||||
- Fix permanent reminders removed on rebooting device
|
||||
- Fixed unable to unpin/disable reminders from notifications
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -176,8 +176,9 @@ export default class Vault {
|
||||
* @param {string} password The password to unlock note with
|
||||
*/
|
||||
async remove(noteId, password) {
|
||||
const note = this._db.notes.note(noteId).data;
|
||||
await this._unlockNote(note, password, true);
|
||||
const note = this._db.notes.note(noteId);
|
||||
if (!note) return;
|
||||
await this._unlockNote(note.data, password, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,8 +187,10 @@ export default class Vault {
|
||||
* @param {string} password The password to open note with
|
||||
*/
|
||||
async open(noteId, password) {
|
||||
const note = this._db.notes.note(noteId).data;
|
||||
const unlockedNote = await this._unlockNote(note, password, false);
|
||||
const note = this._db.notes.note(noteId);
|
||||
if (!note) return;
|
||||
|
||||
const unlockedNote = await this._unlockNote(note.data, password, false);
|
||||
this._password = password;
|
||||
return unlockedNote;
|
||||
}
|
||||
@@ -271,7 +274,10 @@ export default class Vault {
|
||||
async _lockNote(note, password) {
|
||||
let { id, content: { type, data } = {}, sessionId, title } = note;
|
||||
|
||||
note = this._db.notes.note(id).data;
|
||||
note = this._db.notes.note(id);
|
||||
if (!note) return;
|
||||
|
||||
note = note.data;
|
||||
const contentId = note.contentId;
|
||||
if (!contentId) throw new Error("Cannot lock note because it is empty.");
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ export default class Attachments extends Collection {
|
||||
|
||||
async detach(attachment) {
|
||||
await this._db.notes.init();
|
||||
for (let noteId of attachment.noteIds) {
|
||||
for (const noteId of attachment.noteIds) {
|
||||
const note = this._db.notes.note(noteId);
|
||||
if (!note) continue;
|
||||
const contentId = note.data.contentId;
|
||||
@@ -195,9 +195,9 @@ export default class Attachments extends Collection {
|
||||
|
||||
async _canDetach(attachment) {
|
||||
await this._db.notes.init();
|
||||
for (let noteId of attachment.noteIds) {
|
||||
const note = this._db.notes.note(noteId).data;
|
||||
if (note.locked) return false;
|
||||
for (const noteId of attachment.noteIds) {
|
||||
const note = this._db.notes.note(noteId);
|
||||
if (note && note.data.locked) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -85,7 +85,6 @@ export default class NoteHistory extends Collection {
|
||||
if (!noteId || !dateEdited || !content) return;
|
||||
let sessionId = `${noteId}_${dateEdited}`;
|
||||
let oldSession = await this._collection.getItem(sessionId);
|
||||
let locked = this._db.notes.note(noteId)?.data?.locked;
|
||||
|
||||
let session = {
|
||||
type: "session",
|
||||
@@ -96,7 +95,8 @@ export default class NoteHistory extends Collection {
|
||||
localOnly: true
|
||||
};
|
||||
|
||||
if (locked) {
|
||||
const note = this._db.notes.note(noteId);
|
||||
if (note && note.data.locked) {
|
||||
session.locked = true;
|
||||
}
|
||||
|
||||
@@ -178,12 +178,14 @@ export default class NoteHistory extends Collection {
|
||||
/**
|
||||
* @type {Session}
|
||||
*/
|
||||
let session = await this._collection.getItem(sessionId);
|
||||
let content = await this.sessionContent.get(session.sessionContentId);
|
||||
let note = this._db.notes.note(session.noteId).data;
|
||||
const session = await this._collection.getItem(sessionId);
|
||||
const content = await this.sessionContent.get(session.sessionContentId);
|
||||
const note = this._db.notes.note(session.noteId);
|
||||
if (!note) return;
|
||||
|
||||
if (session.locked) {
|
||||
await this._db.content.add({
|
||||
id: note.contentId,
|
||||
id: note.data.contentId,
|
||||
data: content.data,
|
||||
type: content.type
|
||||
});
|
||||
|
||||
@@ -133,9 +133,12 @@ export default class Relations extends Collection {
|
||||
case "reminder":
|
||||
item = this._db.reminders.reminder(reference.id);
|
||||
break;
|
||||
case "note":
|
||||
item = this._db.notes.note(reference.id)?.data;
|
||||
case "note": {
|
||||
const note = this._db.notes.note(reference.id);
|
||||
if (!note) continue;
|
||||
item = note.data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (item) items.push(item);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user