mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
57 Commits
3.0.11-and
...
gen_stack
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb42a2e1a4 | ||
|
|
12c5b6d227 | ||
|
|
364ae51e36 | ||
|
|
18947e0c93 | ||
|
|
3880db4339 | ||
|
|
ae5b0b6fcc | ||
|
|
2c729f90cc | ||
|
|
800514d769 | ||
|
|
b03b9482dc | ||
|
|
9019490dbd | ||
|
|
4a3e23c288 | ||
|
|
1800ebea2d | ||
|
|
a708c9e9eb | ||
|
|
2aeb875fec | ||
|
|
a8164e9f6c | ||
|
|
0ee8e738e5 | ||
|
|
8fdef2ac7d | ||
|
|
920b1d91f3 | ||
|
|
0ad382aced | ||
|
|
c8aa8e37cf | ||
|
|
9a37f7c506 | ||
|
|
717750d5d4 | ||
|
|
1274ecc3b5 | ||
|
|
4c2ad4567c | ||
|
|
ebc01af34a | ||
|
|
e66d8eee13 | ||
|
|
55e6a4b65a | ||
|
|
0aa1db584d | ||
|
|
a0e6266d28 | ||
|
|
bd0aaed863 | ||
|
|
9410c42f29 | ||
|
|
80186c1812 | ||
|
|
37dfda5c54 | ||
|
|
ec83517dc5 | ||
|
|
2eb4911592 | ||
|
|
cd0a6a8fcb | ||
|
|
b15f192244 | ||
|
|
406cb663c9 | ||
|
|
301a30a50a | ||
|
|
81e3432143 | ||
|
|
c43c2936ac | ||
|
|
fdea1807d2 | ||
|
|
5439578fcf | ||
|
|
e05e52f870 | ||
|
|
1c6243d12e | ||
|
|
a51dfb5b6e | ||
|
|
c98bddd35b | ||
|
|
1eb80ccc88 | ||
|
|
5a346aa7de | ||
|
|
910095a769 | ||
|
|
119059befa | ||
|
|
a8f5dea315 | ||
|
|
ca0a4cb02c | ||
|
|
a04cd6b8dc | ||
|
|
3e4ab0f6df | ||
|
|
a7fe1aca44 | ||
|
|
ff0188f602 |
14
.github/workflows/ios.publish.yml
vendored
14
.github/workflows/ios.publish.yml
vendored
@@ -89,13 +89,13 @@ jobs:
|
||||
- name: CCache Stats After Build
|
||||
run: ccache -sv
|
||||
|
||||
- name: "Upload app to TestFlight"
|
||||
uses: apple-actions/upload-testflight-build@v1
|
||||
with:
|
||||
app-path: Notesnook.ipa
|
||||
issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
api-private-key: ${{ secrets.API_KEY }}
|
||||
# - name: "Upload app to TestFlight"
|
||||
# uses: apple-actions/upload-testflight-build@v1
|
||||
# with:
|
||||
# app-path: Notesnook.ipa
|
||||
# issuer-id: ${{ secrets.API_KEY_ISSUER_ID }}
|
||||
# api-key-id: ${{ secrets.API_KEY_ID }}
|
||||
# api-private-key: ${{ secrets.API_KEY }}
|
||||
|
||||
- name: Upload Notesnook.ipa to Github
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
updaterCacheDirName: Notesnook
|
||||
1819
apps/desktop/package-lock.json
generated
1819
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.13",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -16,7 +16,7 @@
|
||||
"@trpc/server": "10.38.3",
|
||||
"better-sqlite3-multiple-ciphers": "^9.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"electron-updater": "^6.2.1",
|
||||
"icojs": "^0.17.1",
|
||||
"sodium-native": "^4.1.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
@@ -28,8 +28,8 @@
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^25.0.0-alpha.11",
|
||||
"esbuild": "^0.20.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.23.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"prebuildify": "^6.0.1",
|
||||
|
||||
@@ -46,11 +46,11 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
if (os.platform() === "win32")
|
||||
await exec(
|
||||
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
);
|
||||
// if (os.platform() === "win32")
|
||||
// await exec(
|
||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
// );
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
|
||||
@@ -61,7 +61,18 @@ export const updaterRouter = t.router({
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available"
|
||||
"update-available",
|
||||
() => {
|
||||
if (!config.automaticUpdates) return false;
|
||||
autoUpdater.emit("download-progress", {
|
||||
bytesPerSecond: 0,
|
||||
delta: 0,
|
||||
percent: 0,
|
||||
total: 100,
|
||||
transferred: 0
|
||||
});
|
||||
return true;
|
||||
}
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
@@ -69,10 +80,11 @@ export const updaterRouter = t.router({
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName) {
|
||||
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
if (handler?.(args[0])) return;
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.removeAllListeners(eventName);
|
||||
|
||||
@@ -35,6 +35,7 @@ async function configureAutoUpdater() {
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
}
|
||||
|
||||
export { configureAutoUpdater };
|
||||
|
||||
@@ -25,7 +25,6 @@ import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { initializeLogger } from "./common/database/logger";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
@@ -41,33 +40,24 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
SettingsService.onFirstLaunch();
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
}, []);
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -23,7 +23,9 @@ import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
let loggerLoaded = false;
|
||||
const initializeLogger = async () => {
|
||||
if (loggerLoaded) return;
|
||||
await initialize({
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
@@ -37,6 +39,7 @@ const initializeLogger = async () => {
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
|
||||
});
|
||||
setLogger();
|
||||
loggerLoaded = true;
|
||||
};
|
||||
|
||||
export { initializeLogger };
|
||||
|
||||
@@ -18,7 +18,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { AppStateStatus, Platform, TextInput, View } from "react-native";
|
||||
import {
|
||||
AppStateStatus,
|
||||
Platform,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { DatabaseLogger } from "../../common/database";
|
||||
@@ -46,6 +52,7 @@ import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -86,6 +93,7 @@ const AppLockedOverlay = () => {
|
||||
const appState = useAppState();
|
||||
const lastAppState = useRef<AppStateStatus>(appState);
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
const { height } = useWindowDimensions();
|
||||
const keyboardType = useSettingStore(
|
||||
(state) => state.settings.applockKeyboardType
|
||||
);
|
||||
@@ -191,15 +199,20 @@ const AppLockedOverlay = () => {
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
return appLocked ? (
|
||||
<View
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
zIndex: 999
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
justifyContent: "center",
|
||||
minHeight: height
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
@@ -314,7 +327,7 @@ const AppLockedOverlay = () => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ import {
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { initializeLogger } from "../common/database/logger";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -280,8 +281,108 @@ const onSubscriptionError = async (error: RNIap.PurchaseError) => {
|
||||
|
||||
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
|
||||
|
||||
const doAppLoadActions = async () => {
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
eSendEvent(eLoginSessionExpired);
|
||||
return;
|
||||
}
|
||||
notifee.setBadgeCount(0);
|
||||
|
||||
if (!(await db.user.getUser())) {
|
||||
setLoginMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
await useMessageStore.getState().setAnnouncement();
|
||||
if (NewFeature.present()) return;
|
||||
if (await checkAppUpdateAvailable()) return;
|
||||
if (await checkForRateAppRequest()) return;
|
||||
if (await PremiumService.getRemainingTrialDaysStatus()) return;
|
||||
if (SettingsService.get().introCompleted) {
|
||||
useMessageStore.subscribe((state) => {
|
||||
const dialogs = state.dialogs;
|
||||
if (dialogs.length > 0) {
|
||||
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkAppUpdateAvailable = async () => {
|
||||
if (__DEV__ || Config.isTesting === "true" || Config.FDROID_BUILD || BETA)
|
||||
return;
|
||||
try {
|
||||
const version =
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? await getGithubVersion()
|
||||
: await checkVersion();
|
||||
if (!version || !version?.needsUpdate) return false;
|
||||
|
||||
setUpdateAvailableMessage(version);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForRateAppRequest = async () => {
|
||||
const rateApp = SettingsService.get().rateApp as number;
|
||||
if (
|
||||
rateApp &&
|
||||
rateApp < Date.now() &&
|
||||
!useMessageStore.getState().message?.visible
|
||||
) {
|
||||
setRateAppMessage();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const IsDatabaseMigrationRequired = () => {
|
||||
if (!db.migrations.required() || useUserStore.getState().appLocked)
|
||||
return false;
|
||||
|
||||
presentSheet({
|
||||
component: <Migrate />,
|
||||
onClose: () => {
|
||||
if (!db.migrations.required()) {
|
||||
initializeDatabase();
|
||||
}
|
||||
},
|
||||
disableClosing: true
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const initializeDatabase = async (password?: string) => {
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
try {
|
||||
await setupDatabase(password);
|
||||
await db.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, "Error initializing database", "global");
|
||||
}
|
||||
}
|
||||
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
|
||||
if (db.isInitialized) {
|
||||
Notifications.setupReminders(true);
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(false);
|
||||
}
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
DatabaseLogger.info("Database initialized");
|
||||
}
|
||||
Walkthrough.init();
|
||||
};
|
||||
|
||||
export const useAppEvents = () => {
|
||||
const loading = useSettingStore((state) => state.isAppLoading);
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [
|
||||
state.setLastSynced,
|
||||
state.setUser,
|
||||
@@ -312,7 +413,7 @@ export const useAppEvents = () => {
|
||||
}, [setLastSynced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (isAppLoading) return;
|
||||
|
||||
let subscriptions: EventManagerSubscription[] = [];
|
||||
const eventManager = db.eventManager;
|
||||
@@ -327,7 +428,7 @@ export const useAppEvents = () => {
|
||||
return () => {
|
||||
subscriptions.forEach((sub) => sub?.unsubscribe?.());
|
||||
};
|
||||
}, [loading, onSyncComplete]);
|
||||
}, [isAppLoading, onSyncComplete]);
|
||||
|
||||
const subscribeToPurchaseListeners = useCallback(async () => {
|
||||
if (Platform.OS === "android") {
|
||||
@@ -572,7 +673,7 @@ export const useAppEvents = () => {
|
||||
});
|
||||
}
|
||||
let sub: NativeEventSubscription;
|
||||
if (!loading && !appLocked) {
|
||||
if (!isAppLoading && !appLocked) {
|
||||
setTimeout(() => {
|
||||
sub = AppState.addEventListener("change", onAppStateChanged);
|
||||
if (
|
||||
@@ -594,7 +695,7 @@ export const useAppEvents = () => {
|
||||
sub?.remove();
|
||||
unsubscribePurchaseListeners();
|
||||
};
|
||||
}, [loading, appLocked, checkAutoBackup]);
|
||||
}, [isAppLoading, appLocked, checkAutoBackup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appLocked && !syncing && refValues.current.backupDidWait) {
|
||||
@@ -643,127 +744,22 @@ export const useAppEvents = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!isAppLoading) {
|
||||
onUserUpdated();
|
||||
doAppLoadActions();
|
||||
}
|
||||
}, [loading, onUserUpdated]);
|
||||
|
||||
const initializeDatabase = useCallback(async (password?: string) => {
|
||||
const IsDatabaseMigrationRequired = () => {
|
||||
if (!db.migrations.required() || useUserStore.getState().appLocked)
|
||||
return false;
|
||||
|
||||
presentSheet({
|
||||
component: <Migrate />,
|
||||
onClose: () => {
|
||||
if (!db.migrations.required()) {
|
||||
initializeDatabase();
|
||||
}
|
||||
},
|
||||
disableClosing: true
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
try {
|
||||
await setupDatabase(password);
|
||||
await db.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, "Error initializing database", "global");
|
||||
}
|
||||
}
|
||||
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
|
||||
if (db.isInitialized) {
|
||||
Notifications.setupReminders(true);
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(false);
|
||||
}
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
DatabaseLogger.info("Database initialized");
|
||||
}
|
||||
Walkthrough.init();
|
||||
}, []);
|
||||
}, [isAppLoading, onUserUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
let sub: () => void;
|
||||
if (appLocked) {
|
||||
const sub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked && useSettingStore.getState().isAppLoading) {
|
||||
console.log("DB initialized");
|
||||
if (!appLocked && isAppLoading) {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
//@ts-ignore
|
||||
initializeDatabase();
|
||||
sub();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
sub?.();
|
||||
};
|
||||
}, [appLocked, initializeDatabase]);
|
||||
|
||||
return initializeDatabase;
|
||||
};
|
||||
|
||||
const doAppLoadActions = async () => {
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
eSendEvent(eLoginSessionExpired);
|
||||
return;
|
||||
}
|
||||
notifee.setBadgeCount(0);
|
||||
|
||||
if (!(await db.user.getUser())) {
|
||||
setLoginMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
await useMessageStore.getState().setAnnouncement();
|
||||
if (NewFeature.present()) return;
|
||||
if (await checkAppUpdateAvailable()) return;
|
||||
if (await checkForRateAppRequest()) return;
|
||||
if (await PremiumService.getRemainingTrialDaysStatus()) return;
|
||||
if (SettingsService.get().introCompleted) {
|
||||
useMessageStore.subscribe((state) => {
|
||||
const dialogs = state.dialogs;
|
||||
if (dialogs.length > 0) {
|
||||
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkAppUpdateAvailable = async () => {
|
||||
if (__DEV__ || Config.isTesting === "true" || Config.FDROID_BUILD || BETA)
|
||||
return;
|
||||
try {
|
||||
const version =
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? await getGithubVersion()
|
||||
: await checkVersion();
|
||||
if (!version || !version?.needsUpdate) return false;
|
||||
|
||||
setUpdateAvailableMessage(version);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForRateAppRequest = async () => {
|
||||
const rateApp = SettingsService.get().rateApp as number;
|
||||
if (
|
||||
rateApp &&
|
||||
rateApp < Date.now() &&
|
||||
!useMessageStore.getState().message?.visible
|
||||
) {
|
||||
setRateAppMessage();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}, [appLocked, isAppLoading]);
|
||||
};
|
||||
|
||||
@@ -62,7 +62,6 @@ import {
|
||||
} from "../services/event-manager";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import {
|
||||
eClearEditor,
|
||||
eCloseFullscreenEditor,
|
||||
eOnEnterEditor,
|
||||
eOnExitEditor,
|
||||
@@ -286,9 +285,10 @@ const _TabsHolder = () => {
|
||||
break;
|
||||
case "mobile":
|
||||
if (
|
||||
state &&
|
||||
!state?.movedAway &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
(state &&
|
||||
!state?.movedAway &&
|
||||
useTabStore.getState().getCurrentNoteId()) ||
|
||||
editorState().movedAway === false
|
||||
) {
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
|
||||
@@ -475,7 +475,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "pull-sync",
|
||||
name: "Force pull changes",
|
||||
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.\n\nThese must only be used for troubleshooting. Using them regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
|
||||
description: `Use this if changes from other devices are not appearing on this device. This will overwrite the data on this device with the latest data from the server.\n\nThis must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: "Force Pull changes",
|
||||
@@ -497,7 +497,7 @@ export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
id: "push-sync",
|
||||
name: "Force push changes",
|
||||
description: `Use this if some changes are not appearing on this device from other devices. This will pull everything from the server and overwrite with whatever is one this device.`,
|
||||
description: `Use this if changes made on this device are not appearing on other devices. This will overwrite the data on the server with the data from this device.\n\nThis must only be used for troubleshooting. Using it regularly for sync is not recommended and will lead to unexpected data loss and other issues. If you are having persistent issues with sync, please report them to us at support@streetwriters.co.`,
|
||||
modifer: () => {
|
||||
presentDialog({
|
||||
title: "Force Push changes",
|
||||
|
||||
@@ -141,7 +141,6 @@ const onBoot = async () => {
|
||||
}
|
||||
|
||||
await Notifications.setupReminders();
|
||||
SettingsService.init();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(false);
|
||||
}
|
||||
|
||||
@@ -171,7 +171,6 @@ async function run(progress = false, context) {
|
||||
}
|
||||
|
||||
let path;
|
||||
let backupFilePath;
|
||||
let backupFileName = "notesnook_backup_" + Date.now();
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
@@ -236,7 +235,7 @@ async function run(progress = false, context) {
|
||||
await sleep(300);
|
||||
|
||||
if (showBackupCompleteSheet) {
|
||||
presentBackupCompleteSheet(backupFilePath);
|
||||
presentBackupCompleteSheet(path);
|
||||
} else {
|
||||
progress && eSendEvent(eCloseSheet);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { NativeModules, Platform } from "react-native";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import {
|
||||
@@ -118,14 +118,18 @@ function setPrivacyScreen(settings: SettingStore["settings"]) {
|
||||
NotesnookModule.setSecureMode(true);
|
||||
} else {
|
||||
enabled(true);
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
if (NativeModules.ScreenGuard) {
|
||||
ScreenGuardModule.register({ backgroundColor: "#000000" });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (Platform.OS === "android") {
|
||||
NotesnookModule.setSecureMode(false);
|
||||
} else {
|
||||
enabled(false);
|
||||
ScreenGuardModule.unregister();
|
||||
if (NativeModules.ScreenGuard) {
|
||||
ScreenGuardModule.unregister();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +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 { Profile } from "@notesnook/core";
|
||||
import { User } from "@notesnook/core";
|
||||
import { Profile, User } from "@notesnook/core";
|
||||
import create, { State } from "zustand";
|
||||
|
||||
export enum SyncStatus {
|
||||
@@ -56,7 +55,9 @@ export const useUserStore = create<UserStore>((set) => ({
|
||||
set({ syncing: syncing, lastSyncStatus: status });
|
||||
},
|
||||
setLastSynced: (lastSynced) => set({ lastSynced: lastSynced }),
|
||||
lockApp: (appLocked) => set({ appLocked }),
|
||||
lockApp: (appLocked) => {
|
||||
set({ appLocked });
|
||||
},
|
||||
lastSyncStatus: SyncStatus.Never,
|
||||
disableAppLockRequests: false,
|
||||
setDisableAppLockRequests: (disableAppLockRequests) => {
|
||||
|
||||
@@ -111,7 +111,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
multiDexEnabled true
|
||||
versionCode 3019
|
||||
versionCode 3020
|
||||
versionName getNpmVersion()
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
- Fixed realtime sync issues
|
||||
- Fixed color popups not opening from main toolbar
|
||||
- Bug fixes and performance improvements
|
||||
|
||||
Thank you for using Notesnook!
|
||||
@@ -21,9 +21,9 @@
|
||||
6517B7C32B6838EB0079FF37 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
|
||||
6529A13E279BC4C70048D4A8 /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6529A13D279BC4C70048D4A8 /* BootSplash.storyboard */; };
|
||||
656835812BB29A9800144BAB /* OpenSans-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 656835802BB29A8300144BAB /* OpenSans-Italic.ttf */; };
|
||||
656DD2AB2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
|
||||
656DD2AC2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
|
||||
656DD2AD2B1891DF00A362EA /* BuildFile in Resources */ = {isa = PBXBuildFile; };
|
||||
6569927F2C5754F10041CD41 /* OpenSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7C02B6838EB0079FF37 /* OpenSans-Bold.ttf */; };
|
||||
656992802C5754F10041CD41 /* OpenSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BE2B6838EB0079FF37 /* OpenSans-Regular.ttf */; };
|
||||
656992812C5754F10041CD41 /* OpenSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6517B7BF2B6838EB0079FF37 /* OpenSans-SemiBold.ttf */; };
|
||||
6593E4A3281C345400492C50 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6593E4A2281C345400492C50 /* AppDelegate.mm */; };
|
||||
659BE46725E11A5100E05671 /* notesnook-text.png in Resources */ = {isa = PBXBuildFile; fileRef = 659BE46625E11A5100E05671 /* notesnook-text.png */; };
|
||||
65AA857925E6DDEC00772A01 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65AA857825E6DDEC00772A01 /* WidgetKit.framework */; };
|
||||
@@ -586,9 +586,9 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
656DD2AB2B1891DF00A362EA /* BuildFile in Resources */,
|
||||
656DD2AC2B1891DF00A362EA /* BuildFile in Resources */,
|
||||
656DD2AD2B1891DF00A362EA /* BuildFile in Resources */,
|
||||
6569927F2C5754F10041CD41 /* OpenSans-Bold.ttf in Resources */,
|
||||
656992802C5754F10041CD41 /* OpenSans-Regular.ttf in Resources */,
|
||||
656992812C5754F10041CD41 /* OpenSans-SemiBold.ttf in Resources */,
|
||||
65C400DF2A80B6B600AA3DF5 /* MaterialCommunityIcons.ttf in Resources */,
|
||||
65C149872A61151B005C40F1 /* extension.bundle in Resources */,
|
||||
65B5014725A672B200E2D264 /* MainInterface.storyboard in Resources */,
|
||||
@@ -1015,7 +1015,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2108;
|
||||
CURRENT_PROJECT_VERSION = 2109;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1089,7 +1089,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.11;
|
||||
MARKETING_VERSION = 3.0.12;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -1120,7 +1120,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 2108;
|
||||
CURRENT_PROJECT_VERSION = 2109;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1194,7 +1194,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.11;
|
||||
MARKETING_VERSION = 3.0.12;
|
||||
ONLY_ACTIVE_ARCH = NO;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -1353,7 +1353,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2108;
|
||||
CURRENT_PROJECT_VERSION = 2109;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1365,7 +1365,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.11;
|
||||
MARKETING_VERSION = 3.0.12;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
@@ -1396,7 +1396,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2108;
|
||||
CURRENT_PROJECT_VERSION = 2109;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1409,7 +1409,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.11;
|
||||
MARKETING_VERSION = 3.0.12;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1439,7 +1439,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Make Note/Make Note.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2108;
|
||||
CURRENT_PROJECT_VERSION = 2109;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 53CWBG3QUC;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.11;
|
||||
MARKETING_VERSION = 3.0.12;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
@@ -1544,7 +1544,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 2108;
|
||||
CURRENT_PROJECT_VERSION = 2109;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 53CWBG3QUC;
|
||||
@@ -1619,7 +1619,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 3.0.11;
|
||||
MARKETING_VERSION = 3.0.12;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
||||
@@ -971,4 +971,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 2b8b28a341b202bf3ca5f231b75bb05893486ed8
|
||||
|
||||
COCOAPODS: 1.14.2
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
118
apps/mobile/package-lock.json
generated
118
apps/mobile/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.11",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"../../packages/common": {
|
||||
"name": "@notesnook/common",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/core": "^8.0.0",
|
||||
@@ -114,7 +114,7 @@
|
||||
},
|
||||
"../../packages/core": {
|
||||
"name": "@notesnook/core",
|
||||
"version": "8.0.2",
|
||||
"version": "8.0.8",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
@@ -3121,7 +3121,7 @@
|
||||
},
|
||||
"../../packages/crypto": {
|
||||
"name": "@notesnook/crypto",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.7",
|
||||
"dev": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -3135,7 +3135,7 @@
|
||||
},
|
||||
"../../packages/editor": {
|
||||
"name": "@notesnook/editor",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.7",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -7163,7 +7163,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/prop-types": {
|
||||
"version": "15.7.11",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/q": {
|
||||
@@ -7183,7 +7183,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/react": {
|
||||
"version": "18.2.39",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -7214,7 +7214,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/scheduler": {
|
||||
"version": "0.16.8",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/@types/semver": {
|
||||
@@ -12072,7 +12072,7 @@
|
||||
},
|
||||
"../../packages/editor-mobile/node_modules/immer": {
|
||||
"version": "9.0.21",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -22461,6 +22461,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"../../packages/editor/node_modules/json-parse-even-better-errors": {
|
||||
@@ -22516,6 +22517,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
@@ -23037,6 +23039,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -23055,6 +23058,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
@@ -23174,6 +23178,7 @@
|
||||
},
|
||||
"../../packages/editor/node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -23795,13 +23800,13 @@
|
||||
},
|
||||
"../../packages/logger": {
|
||||
"name": "@notesnook/logger",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {}
|
||||
},
|
||||
"../../packages/sodium": {
|
||||
"name": "@notesnook/sodium",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.7",
|
||||
"dev": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
@@ -25173,7 +25178,7 @@
|
||||
},
|
||||
"../../packages/theme": {
|
||||
"name": "@notesnook/theme",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -26240,7 +26245,7 @@
|
||||
},
|
||||
"../../packages/ui": {
|
||||
"name": "@notesnook/ui",
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.8",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/theme": "file:../theme"
|
||||
@@ -30892,6 +30897,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -30905,6 +30911,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31051,6 +31058,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-private-property-in-object": {
|
||||
"version": "7.21.0-placeholder-for-preset-env.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -31061,6 +31069,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-proposal-unicode-property-regex": {
|
||||
"version": "7.18.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
@@ -31106,6 +31115,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-class-static-block": {
|
||||
"version": "7.14.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.14.5"
|
||||
@@ -31142,6 +31152,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-export-namespace-from": {
|
||||
"version": "7.8.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.8.3"
|
||||
@@ -31165,6 +31176,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-assertions": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -31178,6 +31190,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-attributes": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -31191,6 +31204,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-import-meta": {
|
||||
"version": "7.10.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
@@ -31201,6 +31215,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-json-strings": {
|
||||
"version": "7.8.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.8.0"
|
||||
@@ -31224,6 +31239,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-logical-assignment-operators": {
|
||||
"version": "7.10.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.10.4"
|
||||
@@ -31284,6 +31300,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-private-property-in-object": {
|
||||
"version": "7.14.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.14.5"
|
||||
@@ -31297,6 +31314,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-top-level-await": {
|
||||
"version": "7.14.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.14.5"
|
||||
@@ -31323,6 +31341,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-unicode-sets-regex": {
|
||||
"version": "7.18.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||
@@ -31350,6 +31369,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-async-generator-functions": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.22.5",
|
||||
@@ -31407,6 +31427,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-properties": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.22.5",
|
||||
@@ -31421,6 +31442,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-class-static-block": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.22.5",
|
||||
@@ -31484,6 +31506,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dotall-regex": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
|
||||
@@ -31498,6 +31521,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-duplicate-keys": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -31511,6 +31535,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-dynamic-import": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31539,6 +31564,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-export-namespace-from": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31595,6 +31621,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-json-strings": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31622,6 +31649,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31649,6 +31677,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-amd": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.22.5",
|
||||
@@ -31678,6 +31707,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-systemjs": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-hoist-variables": "^7.22.5",
|
||||
@@ -31694,6 +31724,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-modules-umd": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.22.5",
|
||||
@@ -31722,6 +31753,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-new-target": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -31735,6 +31767,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31749,6 +31782,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-numeric-separator": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31776,6 +31810,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-object-rest-spread": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.5",
|
||||
@@ -31807,6 +31842,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-optional-catch-binding": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31821,6 +31857,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-optional-chaining": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
@@ -31849,6 +31886,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-private-methods": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.22.5",
|
||||
@@ -31863,6 +31901,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-private-property-in-object": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.22.5",
|
||||
@@ -31962,6 +32001,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-reserved-words": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -32053,6 +32093,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typeof-symbol": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -32082,6 +32123,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-escapes": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
@@ -32095,6 +32137,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-property-regex": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
|
||||
@@ -32123,6 +32166,7 @@
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-sets-regex": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-regexp-features-plugin": "^7.22.5",
|
||||
@@ -32137,6 +32181,7 @@
|
||||
},
|
||||
"node_modules/@babel/preset-env": {
|
||||
"version": "7.22.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.22.5",
|
||||
@@ -32229,6 +32274,7 @@
|
||||
},
|
||||
"node_modules/@babel/preset-env/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -32251,6 +32297,7 @@
|
||||
},
|
||||
"node_modules/@babel/preset-modules": {
|
||||
"version": "0.1.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
@@ -34900,6 +34947,7 @@
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.40.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
@@ -34908,6 +34956,7 @@
|
||||
},
|
||||
"node_modules/@types/eslint-scope": {
|
||||
"version": "3.7.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
@@ -34916,6 +34965,7 @@
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
@@ -35364,6 +35414,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.11.6",
|
||||
@@ -35372,18 +35423,22 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/floating-point-hex-parser": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.11.6",
|
||||
@@ -35393,10 +35448,12 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -35407,6 +35464,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
@@ -35414,6 +35472,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/leb128": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
@@ -35421,10 +35480,12 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/utf8": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -35439,6 +35500,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -35450,6 +35512,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -35460,6 +35523,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -35472,6 +35536,7 @@
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.11.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
@@ -35529,10 +35594,12 @@
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@yarnpkg/lockfile": {
|
||||
@@ -35588,6 +35655,7 @@
|
||||
},
|
||||
"node_modules/acorn-import-assertions": {
|
||||
"version": "1.9.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8"
|
||||
@@ -36692,6 +36760,7 @@
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
@@ -37792,6 +37861,7 @@
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.15.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -37904,6 +37974,7 @@
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
@@ -38234,6 +38305,7 @@
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"esrecurse": "^4.3.0",
|
||||
@@ -38245,6 +38317,7 @@
|
||||
},
|
||||
"node_modules/eslint-scope/node_modules/estraverse": {
|
||||
"version": "4.3.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
@@ -38430,6 +38503,7 @@
|
||||
},
|
||||
"node_modules/esrecurse": {
|
||||
"version": "4.3.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
@@ -38440,6 +38514,7 @@
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
@@ -38447,6 +38522,7 @@
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -39503,6 +39579,7 @@
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/global": {
|
||||
@@ -41746,6 +41823,7 @@
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
@@ -42013,6 +42091,7 @@
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
@@ -44664,6 +44743,7 @@
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
@@ -45448,6 +45528,7 @@
|
||||
},
|
||||
"node_modules/react-test-renderer": {
|
||||
"version": "18.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-is": "^18.2.0",
|
||||
@@ -45460,10 +45541,12 @@
|
||||
},
|
||||
"node_modules/react-test-renderer/node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-test-renderer/node_modules/scheduler": {
|
||||
"version": "0.23.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
@@ -46299,6 +46382,7 @@
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.1",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
@@ -47354,6 +47438,7 @@
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.9",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
@@ -47386,6 +47471,7 @@
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -47398,6 +47484,7 @@
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
@@ -48142,6 +48229,7 @@
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -48164,6 +48252,7 @@
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.88.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
@@ -48278,6 +48367,7 @@
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@notesnook/mobile",
|
||||
"version": "3.0.11",
|
||||
"version": "3.0.12",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
|
||||
46
apps/theme-builder/package-lock.json
generated
46
apps/theme-builder/package-lock.json
generated
@@ -134,15 +134,15 @@
|
||||
},
|
||||
"../../packages/common": {
|
||||
"name": "@notesnook/common",
|
||||
"version": "1.0.0",
|
||||
"version": "2.0.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/core": "file:../core",
|
||||
"@notesnook/core": "^8.0.0",
|
||||
"pathe": "^1.1.2",
|
||||
"timeago.js": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@notesnook/core": "file:../core",
|
||||
"@notesnook/core": "^8.0.0",
|
||||
"@types/react": "^18.2.39",
|
||||
"react": "18.2.0",
|
||||
"vitest": "^1.4.0"
|
||||
@@ -330,7 +330,7 @@
|
||||
},
|
||||
"../../packages/theme": {
|
||||
"name": "@notesnook/theme",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@emotion/react": "11.11.1",
|
||||
@@ -979,7 +979,7 @@
|
||||
},
|
||||
"../web": {
|
||||
"name": "@notesnook/web",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.13",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -1029,12 +1029,12 @@
|
||||
"framer-motion": "^10.16.8",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"hotkeys-js": "^3.8.3",
|
||||
"immer": "^10.0.3",
|
||||
"katex": "0.16.2",
|
||||
"kysely": "^0.26.3",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"mac-scrollbar": "^0.13.5",
|
||||
"marked": "^4.1.0",
|
||||
"mutative": "^1.0.6",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
@@ -1059,12 +1059,14 @@
|
||||
"w3c-keyname": "^2.2.6",
|
||||
"web-streams-polyfill": "^3.1.1",
|
||||
"wouter": "2.12.1",
|
||||
"zustand": "4.4.7"
|
||||
"zustand": "4.4.7",
|
||||
"zustand-mutative": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@swc/core": "1.3.61",
|
||||
"@swc/core": "^1.5.24",
|
||||
"@swc/plugin-react-remove-properties": "^2.0.4",
|
||||
"@trpc/server": "10.38.3",
|
||||
"@types/babel__core": "^7.20.1",
|
||||
"@types/event-source-polyfill": "^1.0.1",
|
||||
@@ -1079,8 +1081,8 @@
|
||||
"@types/react-scroll-sync": "^0.9.0",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@vitejs/plugin-react-swc": "3.3.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"better-sqlite3-multiple-ciphers": "^9.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -1091,14 +1093,13 @@
|
||||
"ip": "^1.1.8",
|
||||
"lorem-ipsum": "^2.0.4",
|
||||
"otplib": "^12.0.1",
|
||||
"rollup": "^3.29.4",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"swc-plugin-react-remove-properties": "^0.1.4",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-env-compatible": "^1.1.1",
|
||||
"vite-plugin-pwa": "^0.16.3",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"vitest": "^0.34.6",
|
||||
"rollup": "^4.18.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"vite": "^5.2.12",
|
||||
"vite-plugin-env-compatible": "^2.0.1",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^1.6.0",
|
||||
"workbox-core": "^7.0.0",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
"workbox-precaching": "^7.0.0",
|
||||
@@ -2027,15 +2028,6 @@
|
||||
"@trpc/server": "10.38.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@trpc/server": {
|
||||
"version": "10.38.3",
|
||||
"resolved": "https://registry.npmjs.org/@trpc/server/-/server-10.38.3.tgz",
|
||||
"integrity": "sha512-9s8/kwo2IDB5hwB2SKZZrfevRhdb1f9fdXtIYd3lbQuf2jQaC/LyQuHaIQjDQoUx9updBfsHXcFFPiCP1DL6pg==",
|
||||
"funding": [
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.1",
|
||||
"dev": true,
|
||||
|
||||
2
apps/vericrypt/package-lock.json
generated
2
apps/vericrypt/package-lock.json
generated
@@ -33,7 +33,7 @@
|
||||
},
|
||||
"../../packages/crypto": {
|
||||
"name": "@notesnook/crypto",
|
||||
"version": "1.1.1",
|
||||
"version": "2.0.7",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/sodium": "file:../sodium"
|
||||
|
||||
1266
apps/web/package-lock.json
generated
1266
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@notesnook/web",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.10",
|
||||
"version": "3.0.13",
|
||||
"private": true,
|
||||
"main": "./src/app.js",
|
||||
"homepage": "https://notesnook.com/",
|
||||
@@ -54,12 +54,12 @@
|
||||
"framer-motion": "^10.16.8",
|
||||
"hash-wasm": "^4.9.0",
|
||||
"hotkeys-js": "^3.8.3",
|
||||
"immer": "^10.0.3",
|
||||
"katex": "0.16.2",
|
||||
"kysely": "^0.26.3",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"mac-scrollbar": "^0.13.5",
|
||||
"marked": "^4.1.0",
|
||||
"mutative": "^1.0.6",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"phone": "^3.1.14",
|
||||
"platform": "^1.3.6",
|
||||
@@ -84,7 +84,8 @@
|
||||
"w3c-keyname": "^2.2.6",
|
||||
"web-streams-polyfill": "^3.1.1",
|
||||
"wouter": "2.12.1",
|
||||
"zustand": "4.4.7"
|
||||
"zustand": "4.4.7",
|
||||
"zustand-mutative": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
|
||||
@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import React, { useEffect } from "react";
|
||||
import { useStore } from "./stores/app-store";
|
||||
import { useStore as useUserStore } from "./stores/user-store";
|
||||
import { useStore as useAttachmentStore } from "./stores/attachment-store";
|
||||
import { useEditorStore } from "./stores/editor-store";
|
||||
import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { resetNotices, scheduleBackups } from "./common/notices";
|
||||
@@ -30,17 +29,17 @@ import { db } from "./common/db";
|
||||
import { EV, EVENTS } from "@notesnook/core/dist/common";
|
||||
import { registerKeyMap } from "./common/key-map";
|
||||
import { isUserPremium } from "./hooks/use-is-user-premium";
|
||||
import {
|
||||
showAnnouncementDialog,
|
||||
showBuyDialog,
|
||||
showFeatureDialog,
|
||||
showOnboardingDialog
|
||||
} from "./common/dialog-controller";
|
||||
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
||||
import { showToast } from "./utils/toast";
|
||||
import { interruptedOnboarding } from "./dialogs/onboarding-dialog";
|
||||
import {
|
||||
interruptedOnboarding,
|
||||
OnboardingDialog
|
||||
} from "./dialogs/onboarding-dialog";
|
||||
import { hashNavigate } from "./navigation";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
import { BuyDialog } from "./dialogs/buy-dialog";
|
||||
import { FeatureDialog } from "./dialogs/feature-dialog";
|
||||
import { AnnouncementDialog } from "./dialogs/announcement-dialog";
|
||||
|
||||
type AppEffectsProps = {
|
||||
setShow: (show: boolean) => void;
|
||||
@@ -51,7 +50,6 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
const isFocusMode = useStore((store) => store.isFocusMode);
|
||||
const initUser = useUserStore((store) => store.init);
|
||||
const initStore = useStore((store) => store.init);
|
||||
const initAttachments = useAttachmentStore((store) => store.init);
|
||||
const setIsVaultCreated = useStore((store) => store.setIsVaultCreated);
|
||||
const initEditorStore = useEditorStore((store) => store.init);
|
||||
const dialogAnnouncements = useAnnouncementStore(
|
||||
@@ -69,7 +67,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
showToast(
|
||||
"error",
|
||||
"Please upgrade your account to Pro to use this feature.",
|
||||
[{ text: "Upgrade now", onClick: () => showBuyDialog() }]
|
||||
[{ text: "Upgrade now", onClick: () => BuyDialog.show({}) }]
|
||||
);
|
||||
return { type, result: false };
|
||||
}
|
||||
@@ -77,7 +75,6 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
);
|
||||
|
||||
initStore();
|
||||
initAttachments();
|
||||
initEditorStore();
|
||||
|
||||
(async function () {
|
||||
@@ -89,8 +86,9 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
await resetNotices();
|
||||
setIsVaultCreated(await db.vault.exists());
|
||||
|
||||
await showOnboardingDialog(interruptedOnboarding());
|
||||
await showFeatureDialog("highlights");
|
||||
const onboardingKey = interruptedOnboarding();
|
||||
if (onboardingKey) await OnboardingDialog.show({ type: onboardingKey });
|
||||
await FeatureDialog.show({ featureName: "highlights" });
|
||||
await scheduleBackups();
|
||||
})();
|
||||
|
||||
@@ -101,7 +99,6 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
[
|
||||
initEditorStore,
|
||||
initStore,
|
||||
initAttachments,
|
||||
updateLastSynced,
|
||||
refreshNavItems,
|
||||
initUser,
|
||||
@@ -224,7 +221,7 @@ export default function AppEffects({ setShow }: AppEffectsProps) {
|
||||
useEffect(() => {
|
||||
if (!dialogAnnouncements.length || IS_TESTING) return;
|
||||
(async () => {
|
||||
await showAnnouncementDialog(dialogAnnouncements[0]);
|
||||
await AnnouncementDialog.show({ announcement: dialogAnnouncements[0] });
|
||||
})();
|
||||
}, [dialogAnnouncements]);
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { EventSourcePolyfill as EventSource } from "event-source-polyfill";
|
||||
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
|
||||
import { logger } from "../utils/logger";
|
||||
import { showMigrationDialog } from "./dialog-controller";
|
||||
import { database } from "@notesnook/common";
|
||||
import { createDialect } from "./sqlite";
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
@@ -119,7 +118,9 @@ async function initializeDatabase(persistence: DatabasePersistence) {
|
||||
logger.measure("Database initialization");
|
||||
|
||||
if (db.migrations?.required()) {
|
||||
await showMigrationDialog();
|
||||
await import("../dialogs/migration-dialog").then(({ MigrationDialog }) =>
|
||||
MigrationDialog.show({})
|
||||
);
|
||||
}
|
||||
|
||||
return db;
|
||||
|
||||
@@ -28,7 +28,10 @@ export const desktop = createTRPCProxyClient<AppRouter>({
|
||||
links: [ipcLink()]
|
||||
});
|
||||
|
||||
document.addEventListener("readystatechange", async () => {
|
||||
attachListeners();
|
||||
function attachListeners() {
|
||||
console.log("attaching listeners");
|
||||
|
||||
desktop.updater.onChecking.subscribe(
|
||||
undefined,
|
||||
attachListener(AppEvents.checkingForUpdate)
|
||||
@@ -62,7 +65,7 @@ document.addEventListener("readystatechange", async () => {
|
||||
TaskScheduler.register("updateCheck", "0 0 */12 * * * *", () => {
|
||||
checkForUpdate();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function attachListener(event: string) {
|
||||
return {
|
||||
|
||||
@@ -1,577 +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 { Dialogs } from "../dialogs";
|
||||
import { store as tagStore } from "../stores/tag-store";
|
||||
import { store as appStore } from "../stores/app-store";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { store as noteStore } from "../stores/note-store";
|
||||
import { db } from "./db";
|
||||
import { showToast } from "../utils/toast";
|
||||
import Config from "../utils/config";
|
||||
import { AppVersion, getChangelog } from "../utils/version";
|
||||
import { Period } from "../dialogs/buy-dialog/types";
|
||||
import { FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { Suspense } from "react";
|
||||
import { ConfirmDialogProps } from "../dialogs/confirm";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { downloadUpdate } from "../utils/updater";
|
||||
import { ThemeMetadata } from "@notesnook/themes-server";
|
||||
import {
|
||||
Color,
|
||||
Profile,
|
||||
Reminder,
|
||||
Tag,
|
||||
AuthenticatorType
|
||||
} from "@notesnook/core";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { PasswordDialogProps } from "../dialogs/password-dialog";
|
||||
import { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
|
||||
type DialogTypes = typeof Dialogs;
|
||||
type DialogIds = keyof DialogTypes;
|
||||
export type Perform<T = boolean> = (result: T) => void;
|
||||
type RenderDialog<TId extends DialogIds, TReturnType> = (
|
||||
dialog: DialogTypes[TId],
|
||||
perform: (result: TReturnType) => void
|
||||
) => JSX.Element;
|
||||
|
||||
let openDialogs: Partial<Record<DialogIds, boolean>> = {};
|
||||
function showDialog<TId extends DialogIds, TReturnType>(
|
||||
id: TId,
|
||||
render: RenderDialog<TId, TReturnType>
|
||||
): Promise<TReturnType> {
|
||||
return new Promise((resolve) => {
|
||||
if (openDialogs[id]) return false;
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.id = id;
|
||||
const root = createRoot(container);
|
||||
|
||||
const perform = (result: TReturnType) => {
|
||||
openDialogs[id] = false;
|
||||
root.unmount();
|
||||
container.remove();
|
||||
resolve(result);
|
||||
};
|
||||
const PropDialog = () => render(Dialogs[id], perform);
|
||||
root.render(
|
||||
<Suspense fallback={<div />}>
|
||||
<PropDialog />
|
||||
</Suspense>
|
||||
);
|
||||
openDialogs[id] = true;
|
||||
});
|
||||
}
|
||||
|
||||
export function closeOpenedDialog() {
|
||||
openDialogs = {};
|
||||
const dialogs = document.querySelectorAll(
|
||||
".ReactModalPortal,[data-react-modal-body-trap]"
|
||||
);
|
||||
dialogs.forEach((elem) => elem.remove());
|
||||
}
|
||||
|
||||
export function showAddTagsDialog(noteIds: string[]) {
|
||||
return showDialog("AddTagsDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res: any) => perform(res)} noteIds={noteIds} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showCreateColorDialog() {
|
||||
return showDialog<"CreateColorDialog", string | undefined>(
|
||||
"CreateColorDialog",
|
||||
(Dialog, perform) => (
|
||||
<Dialog onClose={() => perform(undefined)} onDone={(id) => perform(id)} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function showAddNotebookDialog(parentId?: string) {
|
||||
return showDialog("AddNotebookDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
parentId={parentId}
|
||||
onClose={() => {
|
||||
perform(false);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showNoteLinkingDialog(attr?: LinkAttributes) {
|
||||
return showDialog<"NoteLinkingDialog", LinkAttributes | undefined>(
|
||||
"NoteLinkingDialog",
|
||||
(Dialog, perform) => (
|
||||
<Dialog
|
||||
attributes={attr}
|
||||
onDone={(link) => perform(link)}
|
||||
onClose={() => perform(undefined)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function showEditNotebookDialog(notebookId: string) {
|
||||
const notebook = await db.notebooks.notebook(notebookId);
|
||||
if (!notebook) return;
|
||||
return await showDialog("AddNotebookDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
notebook={notebook}
|
||||
edit={true}
|
||||
onClose={() => {
|
||||
perform(false);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showBuyDialog(plan?: Period, couponCode?: string) {
|
||||
return showDialog("BuyDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
plan={plan}
|
||||
couponCode={couponCode}
|
||||
onClose={() => perform(false)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function confirm<TCheckId extends string>(
|
||||
props: Omit<ConfirmDialogProps<TCheckId>, "onClose">
|
||||
) {
|
||||
return showDialog<"Confirm", false | Record<TCheckId, boolean>>(
|
||||
"Confirm",
|
||||
(Dialog, perform) => (
|
||||
<Dialog {...props} onClose={(result) => perform(result)} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function showPromptDialog(props: {
|
||||
title: string;
|
||||
description?: string;
|
||||
defaultValue?: string;
|
||||
}) {
|
||||
return showDialog<"Prompt", string | null>("Prompt", (Dialog, perform) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
onClose={() => perform(null)}
|
||||
onSave={(text) => {
|
||||
perform(text);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showEmailChangeDialog() {
|
||||
return showDialog("EmailChangeDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={() => perform(null)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showError(title: string, message: string) {
|
||||
return confirm({ title, message, positiveButtonText: "Okay" });
|
||||
}
|
||||
|
||||
export function showMultiDeleteConfirmation(length: number) {
|
||||
return confirm({
|
||||
title: `Delete ${length} items?`,
|
||||
message: `These items will be **kept in your Trash for ${
|
||||
db.settings.getTrashCleanupInterval() || 7
|
||||
} days** after which they will be permanently deleted.`,
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export function showMultiPermanentDeleteConfirmation(length: number) {
|
||||
return confirm({
|
||||
title: `Permanently delete ${length} items?`,
|
||||
message:
|
||||
"These items will be **permanently deleted**. This is IRREVERSIBLE.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export function showLogoutConfirmation() {
|
||||
return confirm({
|
||||
title: `Logout?`,
|
||||
message:
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export function showClearSessionsConfirmation() {
|
||||
return confirm({
|
||||
title: `Logout from other devices?`,
|
||||
message:
|
||||
"All other logged-in devices will be forced to logout stopping sync. Use with care lest you lose important notes.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export function showAccountLoggedOutNotice(reason?: string) {
|
||||
return confirm({
|
||||
title: "You were logged out",
|
||||
message: reason,
|
||||
negativeButtonText: "Okay"
|
||||
});
|
||||
}
|
||||
|
||||
export function showAppUpdatedNotice(
|
||||
version: AppVersion & { changelog?: string }
|
||||
) {
|
||||
return confirm({
|
||||
title: `Welcome to v${version.formatted}`,
|
||||
message: `## Changelog:
|
||||
|
||||
\`\`\`
|
||||
${version.changelog || "No change log."}
|
||||
\`\`\`
|
||||
`,
|
||||
positiveButtonText: `Continue`
|
||||
});
|
||||
}
|
||||
|
||||
export function showEmailVerificationDialog() {
|
||||
return showDialog("EmailVerificationDialog", (Dialog, perform) => (
|
||||
<Dialog onCancel={() => perform(false)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showMigrationDialog() {
|
||||
return showDialog("MigrationDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={() => perform(false)} />
|
||||
));
|
||||
}
|
||||
|
||||
type LoadingDialogProps<T> = {
|
||||
title: string;
|
||||
message?: string;
|
||||
subtitle: string;
|
||||
action: () => T | Promise<T>;
|
||||
};
|
||||
export function showLoadingDialog<T>(dialogData: LoadingDialogProps<T>) {
|
||||
const { title, message, subtitle, action } = dialogData;
|
||||
return showDialog<"LoadingDialog", T | boolean>(
|
||||
"LoadingDialog",
|
||||
(Dialog, perform) => (
|
||||
<Dialog
|
||||
title={title}
|
||||
description={subtitle}
|
||||
message={message}
|
||||
action={action}
|
||||
onClose={(e) => perform(e as T | boolean)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
type ProgressDialogProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
action: (...args: never[]) => void;
|
||||
};
|
||||
export function showProgressDialog<T>(dialogData: ProgressDialogProps) {
|
||||
const { title, subtitle, action } = dialogData;
|
||||
return showDialog<"ProgressDialog", T>(
|
||||
"ProgressDialog",
|
||||
(Dialog, perform) => (
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
action={action}
|
||||
onDone={(e: T) => perform(e)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function showThemeDetails(theme: ThemeMetadata) {
|
||||
return showDialog("ThemeDetailsDialog", (Dialog, perform) => (
|
||||
<Dialog theme={theme} onClose={(res: boolean) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showMoveNoteDialog(noteIds: string[]) {
|
||||
return showDialog("MoveDialog", (Dialog, perform) => (
|
||||
<Dialog noteIds={noteIds} onClose={(res: boolean) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showPasswordDialog<
|
||||
TInputId extends string,
|
||||
TCheckId extends string
|
||||
>(props: Omit<PasswordDialogProps<TInputId, TCheckId>, "onClose">) {
|
||||
return showDialog<
|
||||
"PasswordDialog",
|
||||
string extends TCheckId ? boolean : false | Record<TCheckId, boolean>
|
||||
>("PasswordDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
onClose={(result) =>
|
||||
perform(
|
||||
result as string extends TCheckId
|
||||
? boolean
|
||||
: false | Record<TCheckId, boolean>
|
||||
)
|
||||
}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showBackupPasswordDialog(
|
||||
validate: (outputs: {
|
||||
password?: string;
|
||||
key?: string;
|
||||
}) => boolean | Promise<boolean>
|
||||
) {
|
||||
return showDialog("BackupPasswordDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={() => perform(false)} validate={validate} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showRecoveryKeyDialog() {
|
||||
return showDialog("RecoveryKeyDialog", (Dialog, perform) => (
|
||||
<Dialog onDone={() => perform(true)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showCreateTagDialog() {
|
||||
return showDialog("ItemDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
title={"Create tag"}
|
||||
subtitle={"You can create as many tags as you want."}
|
||||
onClose={() => {
|
||||
perform(false);
|
||||
}}
|
||||
onAction={async (title: string) => {
|
||||
await db.tags.add({ title });
|
||||
showToast("success", "Tag created!");
|
||||
tagStore.refresh();
|
||||
perform(true);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showEditTagDialog(tag: Tag) {
|
||||
return showDialog("ItemDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
title={"Edit tag"}
|
||||
subtitle={`You are editing #${tag.title}.`}
|
||||
defaultValue={tag.title}
|
||||
onClose={() => perform(false)}
|
||||
onAction={async (title: string) => {
|
||||
await db.tags.add({ id: tag.id, title });
|
||||
showToast("success", "Tag edited!");
|
||||
await tagStore.refresh();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await noteStore.refresh();
|
||||
await appStore.refreshNavItems();
|
||||
perform(true);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showRenameColorDialog(color: Color) {
|
||||
return showDialog("ItemDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
title={"Rename color"}
|
||||
subtitle={`You are renaming color ${color.title}.`}
|
||||
defaultValue={color.title}
|
||||
onClose={() => perform(false)}
|
||||
onAction={async (title: string) => {
|
||||
if (!title) return;
|
||||
await db.colors.add({ id: color.id, title });
|
||||
showToast("success", "Color renamed!");
|
||||
appStore.refreshNavItems();
|
||||
perform(true);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showFeatureDialog(featureName: FeatureKeys) {
|
||||
return showDialog("FeatureDialog", (Dialog, perform) => (
|
||||
<Dialog featureName={featureName} onClose={(res) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showReminderDialog(reminderKey: string) {
|
||||
if (Config.get(reminderKey, false)) return;
|
||||
|
||||
return showDialog("ReminderDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
reminderKey={reminderKey}
|
||||
onClose={(res: boolean) => {
|
||||
Config.set(reminderKey, true);
|
||||
perform(res);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showReminderPreviewDialog(reminder: Reminder) {
|
||||
return showDialog("ReminderPreviewDialog", (Dialog, perform) => (
|
||||
<Dialog reminder={reminder} onClose={perform} />
|
||||
));
|
||||
}
|
||||
|
||||
export async function showAddReminderDialog(noteId?: string) {
|
||||
const note = noteId ? await db.notes.note(noteId) : undefined;
|
||||
return showDialog("AddReminderDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res: boolean) => perform(res)} note={note} />
|
||||
));
|
||||
}
|
||||
|
||||
export async function showEditReminderDialog(reminderId: string) {
|
||||
const reminder = await db.reminders.reminder(reminderId);
|
||||
if (!reminder) return null;
|
||||
return showDialog("AddReminderDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res: boolean) => perform(res)} reminder={reminder} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showAnnouncementDialog(announcement: any) {
|
||||
return showDialog("AnnouncementDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
announcement={announcement}
|
||||
onClose={(res: boolean) => perform(res)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showIssueDialog() {
|
||||
return showDialog("IssueDialog", (Dialog, perform) => (
|
||||
<Dialog
|
||||
onClose={(res: boolean) => {
|
||||
perform(res);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
export function showMultifactorDialog(primaryMethod?: AuthenticatorType) {
|
||||
return showDialog("MultifactorDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res) => perform(res)} primaryMethod={primaryMethod} />
|
||||
));
|
||||
}
|
||||
|
||||
export function show2FARecoveryCodesDialog(primaryMethod: AuthenticatorType) {
|
||||
return showDialog("RecoveryCodesDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res) => perform(res)} primaryMethod={primaryMethod} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showAttachmentsDialog() {
|
||||
return showDialog("AttachmentsDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res: boolean) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showEditProfilePictureDialog(profile?: Profile) {
|
||||
return showDialog("EditProfilePictureDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res: boolean) => perform(res)} profile={profile} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showImagePickerDialog(images: File[]): Promise<false | File[]> {
|
||||
if (images.length <= 0) return Promise.resolve(false);
|
||||
|
||||
return showDialog("ImagePickerDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res) => perform(res)} images={images} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showSettings() {
|
||||
return showDialog("SettingsDialog", (Dialog, perform) => (
|
||||
<Dialog onClose={(res: boolean) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
export function showOnboardingDialog(type?: string) {
|
||||
if (!type) return;
|
||||
return showDialog("OnboardingDialog", (Dialog, perform) => (
|
||||
<Dialog type={type} onClose={(res: boolean) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
export async function showInvalidSystemTimeDialog({
|
||||
serverTime,
|
||||
localTime
|
||||
}: {
|
||||
serverTime: number;
|
||||
localTime: number;
|
||||
}) {
|
||||
const result = await confirm({
|
||||
title: "Your system clock is out of sync",
|
||||
subtitle:
|
||||
"Please correct your system date & time and reload the app to avoid syncing issues.",
|
||||
message: `Server time: ${getFormattedDate(serverTime)}
|
||||
Local time: ${getFormattedDate(localTime)}
|
||||
Please sync your system time with [https://time.is](https://time.is).`,
|
||||
positiveButtonText: "Reload app"
|
||||
});
|
||||
if (result) window.location.reload();
|
||||
}
|
||||
|
||||
export async function showUpdateAvailableNotice({
|
||||
version
|
||||
}: {
|
||||
version: string;
|
||||
}) {
|
||||
const changelog = await getChangelog(version);
|
||||
|
||||
return showUpdateDialog({
|
||||
title: `New version available`,
|
||||
subtitle: `v${version} is available for download`,
|
||||
changelog,
|
||||
action: { text: `Update now`, onClick: () => downloadUpdate() }
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateDialogProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
changelog: string;
|
||||
action: {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
};
|
||||
async function showUpdateDialog({
|
||||
title,
|
||||
subtitle,
|
||||
changelog,
|
||||
action
|
||||
}: UpdateDialogProps) {
|
||||
const result = await confirm({
|
||||
title,
|
||||
subtitle,
|
||||
message: changelog,
|
||||
width: 500,
|
||||
positiveButtonText: action.text
|
||||
});
|
||||
if (result && action.onClick) action.onClick();
|
||||
}
|
||||
88
apps/web/src/common/dialog-manager.tsx
Normal file
88
apps/web/src/common/dialog-manager.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
export interface BaseDialogProps<T = unknown> {
|
||||
onClose: (result: T) => void;
|
||||
}
|
||||
|
||||
class _DialogManager {
|
||||
private openedDialogs: Set<React.JSXElementConstructor<any>> = new Set();
|
||||
|
||||
open<
|
||||
Props extends BaseDialogProps<any>,
|
||||
Result extends Props extends BaseDialogProps<infer T> ? T : unknown
|
||||
>(
|
||||
component: React.ComponentType<Props>,
|
||||
props: Omit<Props, "onClose">
|
||||
): Promise<Result | false> {
|
||||
if (this.openedDialogs.has(component)) return Promise.resolve(false);
|
||||
|
||||
const container = document.createElement("div");
|
||||
const root = createRoot(container);
|
||||
|
||||
const close = () => {
|
||||
this.openedDialogs.delete(component);
|
||||
root.unmount();
|
||||
container.remove();
|
||||
};
|
||||
|
||||
const Dialog = component as React.FunctionComponent<
|
||||
Omit<Props, "onClose"> & {
|
||||
onClose: (result: any) => void;
|
||||
}
|
||||
>;
|
||||
this.openedDialogs.add(component);
|
||||
return new Promise<Result>((resolve, reject) =>
|
||||
root.render(
|
||||
<Dialog
|
||||
{...props}
|
||||
onClose={(result: Result) => {
|
||||
try {
|
||||
close();
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
closeAll() {
|
||||
this.openedDialogs.clear();
|
||||
const dialogs = document.querySelectorAll(
|
||||
".ReactModalPortal,[data-react-modal-body-trap]"
|
||||
);
|
||||
dialogs.forEach((elem) => elem.remove());
|
||||
}
|
||||
|
||||
register<Props extends BaseDialogProps<any>>(
|
||||
component: React.ComponentType<Props>
|
||||
) {
|
||||
return {
|
||||
show: (props: Omit<Props, "onClose">) => this.open(component, props)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const DialogManager = new _DialogManager();
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
import Vault from "./vault";
|
||||
import { ExportStream } from "../utils/streams/export-stream";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { confirm } from "./dialog-controller";
|
||||
import { ConfirmDialog } from "../dialogs/confirm";
|
||||
|
||||
export async function exportToPDF(
|
||||
title: string,
|
||||
@@ -101,14 +101,14 @@ export async function exportNotes(
|
||||
}
|
||||
});
|
||||
if (result instanceof Error) {
|
||||
confirm({
|
||||
ConfirmDialog.show({
|
||||
title: `Export failed`,
|
||||
message: result.stack || result.message,
|
||||
positiveButtonText: "Okay"
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
confirm({
|
||||
ConfirmDialog.show({
|
||||
title: `Exported ${result.count} notes`,
|
||||
message:
|
||||
result.errors.length > 0
|
||||
|
||||
@@ -17,12 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
showBackupPasswordDialog,
|
||||
showFeatureDialog,
|
||||
showPasswordDialog,
|
||||
showReminderDialog
|
||||
} from "./dialog-controller";
|
||||
import Config from "../utils/config";
|
||||
import { hashNavigate, getCurrentHash } from "../navigation";
|
||||
import { db } from "./db";
|
||||
@@ -38,12 +32,15 @@ import { TaskManager } from "./task-manager";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { createWritableStream } from "./desktop-bridge";
|
||||
import { createZipStream } from "../utils/streams/zip-stream";
|
||||
import { FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { FeatureDialog, FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { ZipEntry, createUnzipIterator } from "../utils/streams/unzip-stream";
|
||||
import { User } from "@notesnook/core";
|
||||
import { LegacyBackupFile } from "@notesnook/core";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { formatDate } from "@notesnook/core/dist/utils/date";
|
||||
import { showPasswordDialog } from "../dialogs/password-dialog";
|
||||
import { BackupPasswordDialog } from "../dialogs/backup-password-dialog";
|
||||
import { ReminderDialog } from "../dialogs/reminder-dialog";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
@@ -70,7 +67,7 @@ export async function introduceFeatures() {
|
||||
const features: FeatureKeys[] = [];
|
||||
for (const feature of features) {
|
||||
if (!Config.get(`feature:${feature}`)) {
|
||||
await showFeatureDialog(feature);
|
||||
await FeatureDialog.show({ featureName: feature });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,10 +170,12 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
const backup = JSON.parse(await readFile(backupFile));
|
||||
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
await showBackupPasswordDialog(async ({ password, key }) => {
|
||||
if (!password && !key) return false;
|
||||
const error = await restoreWithProgress(backup, password, key);
|
||||
return !error;
|
||||
await BackupPasswordDialog.show({
|
||||
validate: async ({ password, key }) => {
|
||||
if (!password && !key) return false;
|
||||
const error = await restoreWithProgress(backup, password, key);
|
||||
return !error;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await restoreWithProgress(backup);
|
||||
@@ -212,15 +211,15 @@ export async function restoreBackupFile(backupFile: File) {
|
||||
const backup = JSON.parse(await entry.text());
|
||||
if (backup.encrypted) {
|
||||
if (!cachedPassword && !cachedKey) {
|
||||
const result = await showBackupPasswordDialog(
|
||||
async ({ password, key }) => {
|
||||
const result = await BackupPasswordDialog.show({
|
||||
validate: async ({ password, key }) => {
|
||||
if (!password && !key) return false;
|
||||
await db.backup?.import(backup, password, key);
|
||||
cachedPassword = password;
|
||||
cachedKey = key;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
if (!result) break;
|
||||
} else await db.backup?.import(backup, cachedPassword, cachedKey);
|
||||
} else {
|
||||
@@ -318,9 +317,9 @@ export async function showUpgradeReminderDialogs() {
|
||||
const isTrial = user.subscription?.type === SUBSCRIPTION_STATUS.TRIAL;
|
||||
const isBasic = user.subscription?.type === SUBSCRIPTION_STATUS.BASIC;
|
||||
if (isBasic && consumed >= 100) {
|
||||
await showReminderDialog("trialexpired");
|
||||
await ReminderDialog.show({ reminderKey: "trialexpired" });
|
||||
} else if (isTrial && consumed >= 75) {
|
||||
await showReminderDialog("trialexpiring");
|
||||
await ReminderDialog.show({ reminderKey: "trialexpiring" });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { confirm, showMultiDeleteConfirmation } from "./dialog-controller";
|
||||
import { store as noteStore } from "../stores/note-store";
|
||||
import { store as notebookStore } from "../stores/notebook-store";
|
||||
import { store as attachmentStore } from "../stores/attachment-store";
|
||||
@@ -27,6 +26,7 @@ import { showToast } from "../utils/toast";
|
||||
import Vault from "./vault";
|
||||
import { TaskManager } from "./task-manager";
|
||||
import { pluralize } from "@notesnook/common";
|
||||
import { ConfirmDialog, showMultiDeleteConfirmation } from "../dialogs/confirm";
|
||||
|
||||
async function moveNotesToTrash(ids: string[], confirm = true) {
|
||||
if (confirm && !(await showMultiDeleteConfirmation(ids.length))) return;
|
||||
@@ -79,7 +79,7 @@ async function moveNotebooksToTrash(ids: string[]) {
|
||||
|
||||
async function deleteAttachments(ids: string[]) {
|
||||
if (
|
||||
!(await confirm({
|
||||
!(await ConfirmDialog.show({
|
||||
title: "Are you sure?",
|
||||
message:
|
||||
"Are you sure you want to permanently delete these attachments? This action is IRREVERSIBLE.",
|
||||
|
||||
@@ -23,12 +23,12 @@ import { db } from "./db";
|
||||
import { store as appStore } from "../stores/app-store";
|
||||
import { Backup, User, Email, Warn, Icon } from "../components/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { showBuyDialog, showRecoveryKeyDialog } from "./dialog-controller";
|
||||
import { hardNavigate, hashNavigate } from "../navigation";
|
||||
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { TaskScheduler } from "../utils/task-scheduler";
|
||||
import { BuyDialog } from "../dialogs/buy-dialog";
|
||||
import { RecoveryKeyDialog } from "../dialogs/recovery-key-dialog";
|
||||
|
||||
export type NoticeType = "autoBackupsOff" | "login" | "email" | "recoverykey";
|
||||
|
||||
@@ -119,7 +119,7 @@ export const NoticesData: Record<NoticeType, NoticeData> = {
|
||||
key: "autoBackupsOff",
|
||||
title: "Automatic backups disabled",
|
||||
subtitle: "Please upgrade to Pro to enable automatic backups.",
|
||||
action: () => showBuyDialog(),
|
||||
action: () => BuyDialog.show({}),
|
||||
dismissable: true,
|
||||
icon: Backup
|
||||
},
|
||||
@@ -143,7 +143,7 @@ export const NoticesData: Record<NoticeType, NoticeData> = {
|
||||
subtitle: "Keep your recovery key safe",
|
||||
dismissable: true,
|
||||
action: async () => {
|
||||
if (await verifyAccount()) await showRecoveryKeyDialog();
|
||||
if (await verifyAccount()) await RecoveryKeyDialog.show({});
|
||||
},
|
||||
icon: Warn
|
||||
}
|
||||
|
||||
@@ -16,30 +16,37 @@ 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 { immerable, setAutoFreeze } from "immer";
|
||||
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
subscribeWithSelector,
|
||||
persist,
|
||||
PersistOptions
|
||||
} from "zustand/middleware";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
import { GetState, IStore, SetState } from "../stores";
|
||||
setAutoFreeze(false);
|
||||
import { mutative } from "zustand-mutative";
|
||||
|
||||
export function createStore<T>(
|
||||
getStore: (set: SetState<T>, get: GetState<T>) => T
|
||||
) {
|
||||
const store = create<
|
||||
T,
|
||||
[["zustand/subscribeWithSelector", never], ["zustand/immer", never]]
|
||||
[["zustand/subscribeWithSelector", never], ["zustand/mutative", never]]
|
||||
>(
|
||||
subscribeWithSelector(
|
||||
immer((set, get) => {
|
||||
const store = getStore(set, get);
|
||||
(store as any)[immerable] = true;
|
||||
return store;
|
||||
})
|
||||
mutative(
|
||||
(set, get) => {
|
||||
const store = getStore(set, get);
|
||||
return store;
|
||||
},
|
||||
{
|
||||
strict: import.meta.env.DEV,
|
||||
mark: (target, { mutable, immutable }) => {
|
||||
if (!target || typeof target !== "object") return mutable;
|
||||
return immutable;
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
return [store, store.getState()] as const;
|
||||
@@ -54,16 +61,24 @@ export function createPersistedStore<T extends object>(
|
||||
[
|
||||
["zustand/persist", Partial<T>],
|
||||
["zustand/subscribeWithSelector", never],
|
||||
["zustand/immer", never]
|
||||
["zustand/mutative", never]
|
||||
]
|
||||
>(
|
||||
persist(
|
||||
subscribeWithSelector(
|
||||
immer((set, get) => {
|
||||
const store = new Store(set, get);
|
||||
(store as any)[immerable] = true;
|
||||
return store;
|
||||
})
|
||||
mutative(
|
||||
(set, get) => {
|
||||
const store = new Store(set, get);
|
||||
return store;
|
||||
},
|
||||
{
|
||||
strict: import.meta.env.DEV,
|
||||
mark: (target, { mutable, immutable }) => {
|
||||
if (!target || typeof target !== "object") return mutable;
|
||||
return immutable;
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
options
|
||||
)
|
||||
|
||||
@@ -17,8 +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 { ProgressDialog } from "../dialogs/progress-dialog";
|
||||
import { removeStatus, updateStatus } from "../hooks/use-status";
|
||||
import { showProgressDialog } from "./dialog-controller";
|
||||
|
||||
type TaskType = "status" | "modal";
|
||||
type TaskAction<T> = (report: ProgressReportCallback) => T | Promise<T>;
|
||||
@@ -74,11 +74,11 @@ export class TaskManager {
|
||||
return result;
|
||||
}
|
||||
case "modal": {
|
||||
return await showProgressDialog<T | Error>({
|
||||
return (await ProgressDialog.show({
|
||||
title: task.title,
|
||||
subtitle: task.subtitle,
|
||||
action: task.action
|
||||
});
|
||||
})) as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { db } from "./db";
|
||||
import { showPasswordDialog } from "./dialog-controller";
|
||||
import { showPasswordDialog } from "../dialogs/password-dialog";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { VAULT_ERRORS } from "@notesnook/core/dist/api/vault";
|
||||
|
||||
|
||||
@@ -24,10 +24,6 @@ import {
|
||||
Image as RebassImage,
|
||||
Text as RebassText
|
||||
} from "@theme-ui/components";
|
||||
import {
|
||||
closeOpenedDialog,
|
||||
showBuyDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { ANALYTICS_EVENTS, trackEvent } from "../../utils/analytics";
|
||||
import {
|
||||
Sync,
|
||||
@@ -43,6 +39,8 @@ import { store as appStore } from "../../stores/app-store";
|
||||
import { createBackup } from "../../common";
|
||||
import { allowedPlatforms } from "../../stores/announcement-store";
|
||||
import { alpha } from "@theme-ui/color";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog/buy-dialog";
|
||||
import { DialogManager } from "../../common/dialog-manager";
|
||||
|
||||
var margins = [0, 2];
|
||||
var HORIZONTAL_MARGIN = 3;
|
||||
@@ -285,7 +283,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
|
||||
sx={sx}
|
||||
onClick={async () => {
|
||||
if (dismissAnnouncement) dismissAnnouncement();
|
||||
closeOpenedDialog();
|
||||
DialogManager.closeAll();
|
||||
trackEvent(ANALYTICS_EVENTS.announcementCta, action);
|
||||
switch (action.type) {
|
||||
case "link": {
|
||||
@@ -299,7 +297,7 @@ function CalltoAction({ action, variant, sx, dismissAnnouncement }) {
|
||||
}
|
||||
case "promo": {
|
||||
const [coupon, plan] = action.data.split(":");
|
||||
await showBuyDialog(plan, coupon);
|
||||
await BuyDialog.show({ plan, coupon });
|
||||
break;
|
||||
}
|
||||
case "force-sync": {
|
||||
|
||||
@@ -38,10 +38,6 @@ import {
|
||||
Reupload,
|
||||
Uploading
|
||||
} from "../icons";
|
||||
import {
|
||||
closeOpenedDialog,
|
||||
showPromptDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { store, useStore } from "../../stores/attachment-store";
|
||||
import { db } from "../../common/db";
|
||||
import { saveAttachment } from "../../common/attachments";
|
||||
@@ -59,6 +55,8 @@ import { getFormattedDate } from "@notesnook/common";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Attachment as AttachmentType } from "@notesnook/core";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { PromptDialog } from "../../dialogs/prompt";
|
||||
import { DialogManager } from "../../common/dialog-manager";
|
||||
|
||||
const FILE_ICONS: Record<string, Icon> = {
|
||||
"image/": FileImage,
|
||||
@@ -158,8 +156,7 @@ export function Attachment({
|
||||
<td>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
maxWidth: compact ? 180 : "95%"
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{status ? (
|
||||
@@ -211,7 +208,12 @@ export function Attachment({
|
||||
<Text
|
||||
as="td"
|
||||
variant="body"
|
||||
sx={{ color: status ? "accent" : "paragraph" }}
|
||||
sx={{
|
||||
color: status ? "accent" : "paragraph",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis"
|
||||
}}
|
||||
>
|
||||
{status ? (
|
||||
<>
|
||||
@@ -259,7 +261,7 @@ const AttachmentMenuItems: (
|
||||
title: note.title,
|
||||
onClick: () => {
|
||||
useEditorStore.getState().openSession(note.id);
|
||||
closeOpenedDialog();
|
||||
DialogManager.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -291,7 +293,7 @@ const AttachmentMenuItems: (
|
||||
title: "Rename",
|
||||
icon: Rename.path,
|
||||
onClick: async () => {
|
||||
const newName = await showPromptDialog({
|
||||
const newName = await PromptDialog.show({
|
||||
title: "Rename attachment",
|
||||
description: attachment.filename,
|
||||
defaultValue: attachment.filename
|
||||
|
||||
@@ -281,21 +281,21 @@ function TabStrip() {
|
||||
items={sessions}
|
||||
moveItem={(from, to) => {
|
||||
if (from === to) return;
|
||||
useEditorStore.setState((state) => {
|
||||
const isToPinned = state.sessions[to].pinned;
|
||||
const [fromTab] = state.sessions.splice(from, 1);
|
||||
const sessions = useEditorStore.getState().sessions.slice();
|
||||
const isToPinned = sessions[to].pinned;
|
||||
const [fromTab] = sessions.splice(from, 1);
|
||||
|
||||
// if the tab where this tab is being dropped is pinned,
|
||||
// let's pin our tab too.
|
||||
if (isToPinned) {
|
||||
fromTab.pinned = true;
|
||||
fromTab.preview = false;
|
||||
}
|
||||
// unpin the tab if it is moved.
|
||||
else if (fromTab.pinned) fromTab.pinned = false;
|
||||
// if the tab where this tab is being dropped is pinned,
|
||||
// let's pin our tab too.
|
||||
if (isToPinned) {
|
||||
fromTab.pinned = true;
|
||||
fromTab.preview = false;
|
||||
}
|
||||
// unpin the tab if it is moved.
|
||||
else if (fromTab.pinned) fromTab.pinned = false;
|
||||
|
||||
state.sessions.splice(to, 0, fromTab);
|
||||
});
|
||||
sessions.splice(to, 0, fromTab);
|
||||
useEditorStore.setState({ sessions });
|
||||
}}
|
||||
renderItem={({ item: session, index: i }) => (
|
||||
<Tab
|
||||
|
||||
@@ -34,36 +34,28 @@ import { Tag } from "@notesnook/core";
|
||||
type HeaderProps = { readonly: boolean; id: string };
|
||||
function Header(props: HeaderProps) {
|
||||
const { readonly, id } = props;
|
||||
const session = useEditorStore((store) =>
|
||||
store.getActiveSession(["default", "readonly"])
|
||||
const tags = useEditorStore(
|
||||
(store) => store.getActiveSession(["default", "readonly"])?.tags
|
||||
);
|
||||
const tags = session?.tags || [];
|
||||
const refreshTags = useEditorStore((store) => store.refreshTags);
|
||||
|
||||
useEffect(() => {
|
||||
refreshTags();
|
||||
}, [id, refreshTags, session?.type]);
|
||||
|
||||
const setTag = useCallback(
|
||||
async function (noteId: string, tags: Tag[], value: string) {
|
||||
const oldTag = tags.find((t) => t.title === value);
|
||||
if (oldTag) {
|
||||
await db.relations.unlink(oldTag, { type: "note", id: noteId });
|
||||
} else {
|
||||
const id =
|
||||
(await db.tags.find(value))?.id ??
|
||||
(await db.tags.add({ title: value }));
|
||||
await db.relations.add(
|
||||
{ id, type: "tag" },
|
||||
{ type: "note", id: noteId }
|
||||
);
|
||||
await useTagStore.getState().refresh();
|
||||
}
|
||||
await refreshTags();
|
||||
await useNoteStore.getState().refresh();
|
||||
},
|
||||
[refreshTags]
|
||||
);
|
||||
const setTag = useCallback(async function (
|
||||
noteId: string,
|
||||
tags: Tag[],
|
||||
value: string
|
||||
) {
|
||||
const oldTag = tags.find((t) => t.title === value);
|
||||
if (oldTag) {
|
||||
await db.relations.unlink(oldTag, { type: "note", id: noteId });
|
||||
} else {
|
||||
const id =
|
||||
(await db.tags.find(value))?.id ??
|
||||
(await db.tags.add({ title: value }));
|
||||
await db.relations.add({ id, type: "tag" }, { type: "note", id: noteId });
|
||||
await useTagStore.getState().refresh();
|
||||
}
|
||||
await useNoteStore.getState().refresh();
|
||||
},
|
||||
[]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,7 +74,7 @@ function Header(props: HeaderProps) {
|
||||
styles={{ container: { mr: 1 }, text: { fontSize: "body" } }}
|
||||
/>
|
||||
))}
|
||||
{!!session && !readonly && tags ? (
|
||||
{!readonly && tags ? (
|
||||
<Autosuggest
|
||||
sessionId={id}
|
||||
filter={(query) => db.lookup.tags(query).items(10)}
|
||||
|
||||
@@ -64,12 +64,12 @@ import { Freeze } from "react-freeze";
|
||||
import { UnlockView } from "../unlock";
|
||||
import DiffViewer from "../diff-viewer";
|
||||
import TableOfContents from "./table-of-contents";
|
||||
import { showNoteLinkingDialog } from "../../common/dialog-controller";
|
||||
import { scrollIntoViewById } from "@notesnook/editor";
|
||||
import { IEditor } from "./types";
|
||||
import { EditorActionBar } from "./action-bar";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
|
||||
import { NoteLinkingDialog } from "../../dialogs/note-linking-dialog";
|
||||
|
||||
const PDFPreview = React.lazy(() => import("../pdf-preview"));
|
||||
|
||||
@@ -545,8 +545,8 @@ export function Editor(props: EditorProps) {
|
||||
result.forEach((attachment) => editor?.attachFile(attachment));
|
||||
}}
|
||||
onInsertInternalLink={async (attributes) => {
|
||||
const link = await showNoteLinkingDialog(attributes);
|
||||
return link;
|
||||
const link = await NoteLinkingDialog.show({ attributes });
|
||||
return link || undefined;
|
||||
}}
|
||||
>
|
||||
{headless ? null : (
|
||||
@@ -763,13 +763,23 @@ function restoreScrollPosition(session: EditorSession) {
|
||||
const scrollContainer = document.getElementById(`editorScroll_${session.id}`);
|
||||
const scrollPosition = Config.get(`${session.id}:scroll-position`, 0);
|
||||
if (scrollContainer) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (scrollContainer.scrollHeight < scrollPosition)
|
||||
scrollContainer.style.minHeight = `${scrollPosition + 100}px`;
|
||||
scrollContainer.scrollTop = scrollPosition;
|
||||
if (scrollContainer.scrollHeight < scrollPosition) {
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (scrollContainer.scrollHeight >= scrollPosition) {
|
||||
observer.disconnect();
|
||||
requestAnimationFrame(
|
||||
() => (scrollContainer.scrollTop = scrollPosition)
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(scrollContainer);
|
||||
// eslint-disable-next-line no-var
|
||||
var timeout = setTimeout(() => {
|
||||
observer.disconnect();
|
||||
}, 30 * 1000);
|
||||
} else
|
||||
requestAnimationFrame(() => (scrollContainer.scrollTop = scrollPosition));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,22 +20,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { SerializedKey } from "@notesnook/crypto/dist/src/types";
|
||||
import { AppEventManager, AppEvents } from "../../common/app-events";
|
||||
import { db } from "../../common/db";
|
||||
import {
|
||||
showBuyDialog,
|
||||
showImagePickerDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { TaskManager } from "../../common/task-manager";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { showFilePicker } from "../../utils/file-picker";
|
||||
import { Attachment } from "@notesnook/editor";
|
||||
import { ImagePickerDialog } from "../../dialogs/image-picker-dialog";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog";
|
||||
|
||||
const FILE_SIZE_LIMIT = 500 * 1024 * 1024;
|
||||
const IMAGE_SIZE_LIMIT = 50 * 1024 * 1024;
|
||||
|
||||
export async function insertAttachments(type = "*/*") {
|
||||
if (!isUserPremium()) {
|
||||
await showBuyDialog();
|
||||
await BuyDialog.show({});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,14 +47,17 @@ export async function insertAttachments(type = "*/*") {
|
||||
|
||||
export async function attachFiles(files: File[]) {
|
||||
if (!isUserPremium()) {
|
||||
await showBuyDialog();
|
||||
await BuyDialog.show({});
|
||||
return;
|
||||
}
|
||||
|
||||
const images =
|
||||
(await showImagePickerDialog(
|
||||
files.filter((f) => f.type.startsWith("image/"))
|
||||
)) || [];
|
||||
let images = files.filter((f) => f.type.startsWith("image/"));
|
||||
images =
|
||||
images.length > 0
|
||||
? (await ImagePickerDialog.show({
|
||||
images
|
||||
})) || []
|
||||
: [];
|
||||
const documents = files.filter((f) => !f.type.startsWith("image/"));
|
||||
const attachments: Attachment[] = [];
|
||||
for (const file of [...images, ...documents]) {
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
import { IEditor, MAX_AUTO_SAVEABLE_WORDS } from "./types";
|
||||
import { useEditorConfig, useToolbarConfig, useEditorManager } from "./manager";
|
||||
import { useIsUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { showBuyDialog } from "../../common/dialog-controller";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
import { debounce } from "@notesnook/common";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
@@ -62,6 +61,7 @@ import Skeleton from "react-loading-skeleton";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import useTablet from "../../hooks/use-tablet";
|
||||
import { TimeFormat } from "@notesnook/core/dist/utils/date";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -155,7 +155,7 @@ function TipTap(props: TipTapProps) {
|
||||
premium: isUserPremium
|
||||
},
|
||||
onPermissionDenied: (claim) => {
|
||||
if (claim === "premium") showBuyDialog();
|
||||
if (claim === "premium") BuyDialog.show({});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useEditorStore } from "../../stores/editor-store";
|
||||
import { debounceWithId } from "@notesnook/common";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import useTablet from "../../hooks/use-tablet";
|
||||
import { useEditorConfig } from "./manager";
|
||||
import { useEditorConfig, useEditorManager } from "./manager";
|
||||
import { getFontById } from "@notesnook/editor";
|
||||
import { replaceDateTime } from "@notesnook/editor/dist/extensions/date-time";
|
||||
import { useStore as useSettingsStore } from "../../stores/setting-store";
|
||||
@@ -131,6 +131,13 @@ function TitleBox(props: TitleBoxProps) {
|
||||
color: "placeholder"
|
||||
}
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const context = useEditorManager.getState().getEditor(id);
|
||||
if (!context) return;
|
||||
context.editor?.focus({ scrollIntoView: true });
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
pendingChanges.current = true;
|
||||
e.target.value = replaceDateTime(
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
useErrorBoundary
|
||||
} from "react-error-boundary";
|
||||
import { useKeyStore } from "../../interfaces/key-store";
|
||||
import { isFeatureSupported } from "../../utils/feature-check";
|
||||
|
||||
export function GlobalErrorHandler(props: PropsWithChildren) {
|
||||
const { showBoundary } = useErrorBoundary();
|
||||
@@ -199,11 +200,7 @@ function getErrorHelp(props: FallbackProps) {
|
||||
action:
|
||||
"This error can only be fixed by wiping & reseting the database. Beware that this will wipe all your data inside the database with no way to recover it later on. This WILL NOT change/affect/delete/wipe your data on the server but ONLY on this device.",
|
||||
fix: async () => {
|
||||
const { createDialect } = await import("../../common/sqlite");
|
||||
await useKeyStore.getState().clear();
|
||||
const dialect = createDialect("notesnook", true);
|
||||
const driver = dialect.createDriver();
|
||||
await driver.delete();
|
||||
await resetDatabase();
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
@@ -213,11 +210,7 @@ function getErrorHelp(props: FallbackProps) {
|
||||
action:
|
||||
"This error can only be fixed by wiping & reseting the Key Store and the database. This WILL NOT change/affect/delete/wipe your data on the server but ONLY on this device.",
|
||||
fix: async () => {
|
||||
const { createDialect } = await import("../../common/sqlite");
|
||||
await useKeyStore.getState().clear();
|
||||
const dialect = createDialect("notesnook", true);
|
||||
const driver = dialect.createDriver();
|
||||
await driver.delete();
|
||||
await resetDatabase();
|
||||
resetErrorBoundary();
|
||||
}
|
||||
};
|
||||
@@ -242,3 +235,17 @@ function errorToString(error: unknown) {
|
||||
? error
|
||||
: JSON.stringify(error);
|
||||
}
|
||||
|
||||
async function resetDatabase() {
|
||||
const { createDialect } = await import("../../common/sqlite");
|
||||
const multiTab = !!globalThis.SharedWorker && isFeatureSupported("opfs");
|
||||
await useKeyStore.getState().clear();
|
||||
const dialect = createDialect({
|
||||
name: "notesnook",
|
||||
encrypted: true,
|
||||
async: !isFeatureSupported("opfs"),
|
||||
multiTab
|
||||
});
|
||||
const driver = dialect.createDriver();
|
||||
await driver.delete();
|
||||
}
|
||||
|
||||
@@ -213,7 +213,8 @@ import {
|
||||
mdiFormatListBulleted,
|
||||
mdiLink,
|
||||
mdiWindowClose,
|
||||
mdiFileMusicOutline
|
||||
mdiFileMusicOutline,
|
||||
mdiBroom
|
||||
} from "@mdi/js";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Theme } from "@notesnook/theme";
|
||||
@@ -548,3 +549,4 @@ export const WindowRestore = createIcon(
|
||||
"M8 16V4h12v12Zm2-2h8V6h-8Zm-6 6V8.525h2V18h9.475v2Zm6-6V6v8Z"
|
||||
);
|
||||
export const WindowClose = createIcon(mdiWindowClose);
|
||||
export const ClearCache = createIcon(mdiBroom);
|
||||
|
||||
@@ -43,7 +43,6 @@ import NavigationItem, { SortableNavigationItem } from "./navigation-item";
|
||||
import { hardNavigate, hashNavigate, navigate } from "../../navigation";
|
||||
import { db } from "../../common/db";
|
||||
import useMobile from "../../hooks/use-mobile";
|
||||
import { showRenameColorDialog } from "../../common/dialog-controller";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useThemeStore } from "../../stores/theme-store";
|
||||
@@ -72,6 +71,7 @@ import { MenuItem } from "@notesnook/ui";
|
||||
import { Notebook, Tag } from "@notesnook/core";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { Menu } from "../../hooks/use-menu";
|
||||
import { RenameColorDialog } from "../../dialogs/item-dialog";
|
||||
|
||||
type Route = {
|
||||
id: string;
|
||||
@@ -368,7 +368,7 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
type: "button",
|
||||
key: "rename-color",
|
||||
title: "Rename color",
|
||||
onClick: () => showRenameColorDialog(color)
|
||||
onClick: () => RenameColorDialog.show(color)
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
|
||||
@@ -54,13 +54,6 @@ import {
|
||||
} from "../icons";
|
||||
import TimeAgo from "../time-ago";
|
||||
import ListItem from "../list-item";
|
||||
import {
|
||||
confirm,
|
||||
showAddReminderDialog,
|
||||
showAddTagsDialog,
|
||||
showCreateColorDialog,
|
||||
showMoveNoteDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { store } from "../../stores/note-store";
|
||||
import { store as userstore } from "../../stores/user-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
@@ -97,12 +90,16 @@ import { SchemeColors } from "@notesnook/theme";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
import Vault from "../../common/vault";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { AddTagsDialog } from "../../dialogs/add-tags-dialog";
|
||||
import { CreateColorDialog } from "../../dialogs/create-color-dialog";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { MoveNoteDialog } from "../../dialogs/move-note-dialog";
|
||||
import { AddReminderDialog } from "../../dialogs/add-reminder-dialog";
|
||||
|
||||
type NoteProps = NoteResolvedData & {
|
||||
item: NoteType;
|
||||
context?: Context;
|
||||
date: number;
|
||||
simplified?: boolean;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
@@ -116,7 +113,6 @@ function Note(props: NoteProps) {
|
||||
item,
|
||||
date,
|
||||
reminder,
|
||||
simplified,
|
||||
compact,
|
||||
context
|
||||
} = props;
|
||||
@@ -130,7 +126,6 @@ function Note(props: NoteProps) {
|
||||
draggable={true}
|
||||
isFocused={isOpened}
|
||||
isCompact={compact}
|
||||
isSimple={simplified}
|
||||
item={note}
|
||||
title={note.title}
|
||||
body={note.headline as string}
|
||||
@@ -391,7 +386,7 @@ const menuItems: (
|
||||
title: "Remind me",
|
||||
icon: AddReminder.path,
|
||||
onClick: async () => {
|
||||
await showAddReminderDialog(note.id);
|
||||
await AddReminderDialog.show({ note });
|
||||
}
|
||||
},
|
||||
{ key: "sep1", type: "separator" },
|
||||
@@ -541,7 +536,7 @@ const menuItems: (
|
||||
onClick: async () => {
|
||||
if (
|
||||
note.localOnly ||
|
||||
(await confirm({
|
||||
(await ConfirmDialog.show({
|
||||
title: `Prevent ${pluralize(ids.length, "note")} from syncing?`,
|
||||
message: `${pluralize(
|
||||
ids.length,
|
||||
@@ -580,7 +575,7 @@ function colorsToMenuItems(
|
||||
title: "Add new color",
|
||||
icon: Plus.path,
|
||||
onClick: async () => {
|
||||
const id = await showCreateColorDialog();
|
||||
const id = await CreateColorDialog.show({});
|
||||
if (!id) return;
|
||||
await store.get().setColor(id, noteColor?.id === id, ...ids);
|
||||
}
|
||||
@@ -620,7 +615,7 @@ function notebooksMenuItems(ids: string[]): MenuItem[] {
|
||||
key: "link-notebooks",
|
||||
title: "Link to...",
|
||||
icon: AddToNotebook.path,
|
||||
onClick: () => showMoveNoteDialog(ids)
|
||||
onClick: () => MoveNoteDialog.show({ noteIds: ids })
|
||||
},
|
||||
{
|
||||
type: "lazy-loader",
|
||||
@@ -700,7 +695,7 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
key: "assign-tags",
|
||||
title: "Assign to...",
|
||||
icon: Plus.path,
|
||||
onClick: () => showAddTagsDialog(ids)
|
||||
onClick: () => AddTagsDialog.show({ noteIds: ids })
|
||||
},
|
||||
{
|
||||
type: "lazy-loader",
|
||||
@@ -732,7 +727,6 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
await db.relations.to({ id, type: "note" }, "tag").unlink();
|
||||
}
|
||||
tagStore.get().refresh();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
}
|
||||
},
|
||||
@@ -753,7 +747,6 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
await db.relations.add(tag, { id, type: "note" });
|
||||
}
|
||||
await tagStore.get().refresh();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
}
|
||||
});
|
||||
@@ -774,7 +767,6 @@ function tagsMenuItems(ids: string[]): MenuItem[] {
|
||||
await db.relations.unlink(tag, { id, type: "note" });
|
||||
}
|
||||
await tagStore.get().refresh();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,12 +38,12 @@ import { hashNavigate, navigate } from "../../navigation";
|
||||
import IconTag from "../icon-tag";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { pluralize } from "@notesnook/common";
|
||||
import { confirm } from "../../common/dialog-controller";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Notebook as NotebookType } from "@notesnook/core";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { useDragHandler } from "../../hooks/use-drag-handler";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
|
||||
type NotebookProps = {
|
||||
item: NotebookType;
|
||||
@@ -202,7 +202,7 @@ export const notebookMenuItems: (
|
||||
variant: "dangerous",
|
||||
icon: Trash.path,
|
||||
onClick: async () => {
|
||||
const result = await confirm({
|
||||
const result = await ConfirmDialog.show({
|
||||
title: `Delete ${pluralize(ids.length, "notebook")}?`,
|
||||
positiveButtonText: `Yes`,
|
||||
negativeButtonText: "No",
|
||||
|
||||
@@ -606,7 +606,7 @@ function Notebooks({ noteId }: { noteId: string }) {
|
||||
renderItem={({ index }) => (
|
||||
<ResolvedItem index={index} items={result.value} type="notebook">
|
||||
{({ item, data }) => (
|
||||
<ListItemWrapper item={item} data={data} simplified />
|
||||
<ListItemWrapper item={item} data={data} compact />
|
||||
)}
|
||||
</ResolvedItem>
|
||||
)}
|
||||
@@ -635,7 +635,7 @@ function Reminders({ noteId }: { noteId: string }) {
|
||||
renderItem={({ index }) => (
|
||||
<ResolvedItem index={index} items={result.value} type="reminder">
|
||||
{({ item, data }) => (
|
||||
<ListItemWrapper item={item} data={data} simplified />
|
||||
<ListItemWrapper item={item} data={data} compact />
|
||||
)}
|
||||
</ResolvedItem>
|
||||
)}
|
||||
@@ -659,6 +659,7 @@ function Attachments({ noteId }: { noteId: string }) {
|
||||
estimatedSize={30}
|
||||
getItemKey={(index) => result.value.key(index)}
|
||||
items={result.value.placeholders}
|
||||
style={{ tableLayout: "fixed", width: "100%" }}
|
||||
header={
|
||||
<tr>
|
||||
<th style={{ width: "75%" }} />
|
||||
@@ -666,7 +667,6 @@ function Attachments({ noteId }: { noteId: string }) {
|
||||
<th style={{ width: "20%" }} />
|
||||
</tr>
|
||||
}
|
||||
headerSize={0}
|
||||
renderRow={({ index }) => (
|
||||
<ResolvedItem index={index} type="attachment" items={result.value}>
|
||||
{({ item }) => <ListItemWrapper item={item} compact />}
|
||||
|
||||
@@ -37,14 +37,12 @@ import { hashNavigate } from "../../navigation";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { store } from "../../stores/reminder-store";
|
||||
import { db } from "../../common/db";
|
||||
import {
|
||||
confirm,
|
||||
showEditReminderDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { pluralize } from "@notesnook/common";
|
||||
import { getFormattedReminderTime } from "@notesnook/common";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Reminder as ReminderType } from "@notesnook/core/dist/types";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { EditReminderDialog } from "../../dialogs/add-reminder-dialog";
|
||||
|
||||
const RECURRING_MODE_MAP = {
|
||||
week: "Weekly",
|
||||
@@ -75,7 +73,7 @@ function Reminder(props: ReminderProps) {
|
||||
body={reminder.description}
|
||||
isDisabled={reminder.disabled}
|
||||
isCompact={compact}
|
||||
onClick={() => showEditReminderDialog(reminder.id)}
|
||||
onClick={() => EditReminderDialog.show({ reminderId: reminder.id })}
|
||||
footer={
|
||||
<Flex
|
||||
sx={{
|
||||
@@ -145,7 +143,7 @@ const menuItems: (reminder: ReminderType, items?: string[]) => MenuItem[] = (
|
||||
variant: "dangerous",
|
||||
icon: Trash.path,
|
||||
onClick: async () => {
|
||||
confirm({
|
||||
ConfirmDialog.show({
|
||||
title: `Delete ${pluralize(items.length, "reminder")}`,
|
||||
message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.`,
|
||||
positiveButtonText: "Yes",
|
||||
|
||||
@@ -117,7 +117,14 @@ function Header(props: RouteContainerProps) {
|
||||
height: 42.8
|
||||
}}
|
||||
>
|
||||
<Flex py={1} sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||
<Flex
|
||||
py={1}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{buttons?.back ? (
|
||||
<ArrowLeft
|
||||
size={24}
|
||||
|
||||
@@ -34,15 +34,13 @@ import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { useStore as useAppStore } from "../../stores/app-store";
|
||||
import { hardNavigate, hashNavigate } from "../../navigation";
|
||||
import { useAutoUpdater, UpdateStatus } from "../../hooks/use-auto-updater";
|
||||
import {
|
||||
showIssueDialog,
|
||||
showUpdateAvailableNotice
|
||||
} from "../../common/dialog-controller";
|
||||
import useStatus, { statusToString } from "../../hooks/use-status";
|
||||
import { ScopedThemeProvider } from "../theme-provider";
|
||||
import { checkForUpdate, installUpdate } from "../../utils/updater";
|
||||
import { getTimeAgo, toTitleCase } from "@notesnook/common";
|
||||
import { User } from "@notesnook/core";
|
||||
import { showUpdateAvailableNotice } from "../../dialogs/confirm";
|
||||
import { IssueDialog } from "../../dialogs/issue-dialog";
|
||||
|
||||
function StatusBar() {
|
||||
const user = useUserStore((state) => state.user);
|
||||
@@ -114,7 +112,7 @@ function StatusBar() {
|
||||
) : null}
|
||||
<Button
|
||||
variant="statusitem"
|
||||
onClick={() => showIssueDialog()}
|
||||
onClick={() => IssueDialog.show({})}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
@@ -24,11 +24,11 @@ import { Notebook } from "@notesnook/core";
|
||||
import { notebookMenuItems } from "../notebook";
|
||||
import { ChevronDown, ChevronRight, Plus } from "../icons";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { showAddNotebookDialog } from "../../common/dialog-controller";
|
||||
import { navigate } from "../../navigation";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { useDragHandler } from "../../hooks/use-drag-handler";
|
||||
import { AddNotebookDialog } from "../../dialogs/add-notebook-dialog";
|
||||
|
||||
type SubNotebookProps = {
|
||||
item: Notebook;
|
||||
@@ -173,7 +173,6 @@ const subNotebookMenuItems: (
|
||||
context?: { refresh?: () => void }
|
||||
) => MenuItem[] = (notebook, ids = [], context) => {
|
||||
const menuItems = notebookMenuItems(notebook, ids);
|
||||
console.log("ITEMS", context);
|
||||
return [
|
||||
{
|
||||
type: "button",
|
||||
@@ -181,7 +180,9 @@ const subNotebookMenuItems: (
|
||||
title: "New notebook",
|
||||
icon: Plus.path,
|
||||
onClick: () =>
|
||||
showAddNotebookDialog(notebook.id).then(() => context?.refresh?.())
|
||||
AddNotebookDialog.show({ parentId: notebook.id }).then((res) =>
|
||||
res ? context?.refresh?.() : null
|
||||
)
|
||||
},
|
||||
{ type: "separator", key: "sepep2" },
|
||||
...menuItems
|
||||
|
||||
@@ -23,15 +23,14 @@ import { Text } from "@theme-ui/components";
|
||||
import { store as appStore } from "../../stores/app-store";
|
||||
import { store as tagStore } from "../../stores/tag-store";
|
||||
import { store as noteStore } from "../../stores/note-store";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { db } from "../../common/db";
|
||||
import { Edit, Shortcut, DeleteForver } from "../icons";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { pluralize } from "@notesnook/common";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Tag as TagType } from "@notesnook/core";
|
||||
import { showEditTagDialog } from "../../common/dialog-controller";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { EditTagDialog } from "../../dialogs/item-dialog";
|
||||
|
||||
type TagProps = { item: TagType; totalNotes: number };
|
||||
function Tag(props: TagProps) {
|
||||
@@ -78,9 +77,7 @@ const menuItems: (tag: TagType, ids?: string[]) => MenuItem[] = (
|
||||
key: "edit",
|
||||
title: "Rename tag",
|
||||
icon: Edit.path,
|
||||
onClick: () => {
|
||||
showEditTagDialog(tag);
|
||||
}
|
||||
onClick: () => EditTagDialog.show(tag)
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
@@ -102,7 +99,6 @@ const menuItems: (tag: TagType, ids?: string[]) => MenuItem[] = (
|
||||
await db.tags.remove(...ids);
|
||||
showToast("success", `${pluralize(ids.length, "tag")} deleted`);
|
||||
await appStore.refreshNavItems();
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await tagStore.refresh();
|
||||
await noteStore.refresh();
|
||||
},
|
||||
|
||||
@@ -18,11 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { showBuyDialog } from "../../common/dialog-controller";
|
||||
import Tip from "../tip";
|
||||
import { isUserPremium } from "../../hooks/use-is-user-premium";
|
||||
import { Flex, Switch } from "@theme-ui/components";
|
||||
import { Loading } from "../icons";
|
||||
import { BuyDialog } from "../../dialogs/buy-dialog/buy-dialog";
|
||||
|
||||
function Toggle(props) {
|
||||
const {
|
||||
@@ -46,7 +46,7 @@ function Toggle(props) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
await showBuyDialog();
|
||||
await BuyDialog.show({});
|
||||
}
|
||||
}, [onToggled, premium, isToggled]);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ListItem from "../list-item";
|
||||
import { showMultiPermanentDeleteConfirmation } from "../../common/dialog-controller";
|
||||
import { Restore, DeleteForver } from "../icons";
|
||||
import { store } from "../../stores/trash-store";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
@@ -28,6 +27,7 @@ import { showToast } from "../../utils/toast";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { TrashItem as TrashItemType } from "@notesnook/core/dist/types";
|
||||
import { useEditorStore } from "../../stores/editor-store";
|
||||
import { showMultiPermanentDeleteConfirmation } from "../../dialogs/confirm";
|
||||
|
||||
type TrashItemProps = { item: TrashItemType; date: number };
|
||||
function TrashItem(props: TrashItemProps) {
|
||||
|
||||
@@ -17,9 +17,10 @@ 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 { Virtualizer, useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { Box } from "@theme-ui/components";
|
||||
import { Virtualizer } from "@tanstack/react-virtual";
|
||||
import { Flex } from "@theme-ui/components";
|
||||
import React, { useRef } from "react";
|
||||
import { TableVirtuoso } from "react-virtuoso";
|
||||
|
||||
export type VirtualizedTableRowProps<T, C> = {
|
||||
item: T;
|
||||
@@ -36,12 +37,10 @@ type VirtualizedTableProps<T, C> = {
|
||||
mode?: "fixed" | "dynamic";
|
||||
items: T[];
|
||||
estimatedSize: number;
|
||||
headerSize: number;
|
||||
getItemKey: (index: number) => string;
|
||||
scrollElement?: Element | null;
|
||||
scrollElement?: HTMLElement | null;
|
||||
context?: C;
|
||||
renderRow: (props: VirtualizedTableRowProps<T, C>) => JSX.Element | null;
|
||||
scrollMargin?: number;
|
||||
header: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
@@ -50,59 +49,95 @@ export function VirtualizedTable<T, C>(props: VirtualizedTableProps<T, C>) {
|
||||
items,
|
||||
getItemKey,
|
||||
scrollElement,
|
||||
scrollMargin,
|
||||
headerSize,
|
||||
renderRow: Row,
|
||||
estimatedSize,
|
||||
mode,
|
||||
virtualizerRef,
|
||||
header,
|
||||
style,
|
||||
context
|
||||
context,
|
||||
style
|
||||
} = props;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: items.length,
|
||||
estimateSize: () => estimatedSize,
|
||||
getItemKey,
|
||||
getScrollElement: () =>
|
||||
scrollElement || containerRef.current?.closest(".ms-container") || null,
|
||||
scrollMargin: scrollMargin || containerRef.current?.offsetTop || 0
|
||||
});
|
||||
|
||||
if (virtualizerRef) virtualizerRef.current = virtualizer;
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems();
|
||||
return (
|
||||
<Box
|
||||
<Flex
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
height: virtualizer.getTotalSize() + headerSize
|
||||
}}
|
||||
variant="columnFill"
|
||||
sx={{ height: estimatedSize * items.length }}
|
||||
>
|
||||
<table style={style}>
|
||||
<thead>{header}</thead>
|
||||
<tbody>
|
||||
{virtualItems.map((row, index) => (
|
||||
<Row
|
||||
key={row.key}
|
||||
item={items[row.index]}
|
||||
index={row.index}
|
||||
rowRef={mode === "dynamic" ? virtualizer.measureElement : null}
|
||||
context={context}
|
||||
style={{
|
||||
height: mode === "dynamic" ? "unset" : `${row.size}px`,
|
||||
transform: `translateY(${
|
||||
row.start -
|
||||
index * row.size -
|
||||
virtualizer.options.scrollMargin
|
||||
}px)`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Box>
|
||||
<TableVirtuoso
|
||||
data={items}
|
||||
context={context}
|
||||
customScrollParent={
|
||||
scrollElement ||
|
||||
containerRef.current?.closest(".ms-container") ||
|
||||
undefined
|
||||
}
|
||||
computeItemKey={(index) => getItemKey(index)}
|
||||
defaultItemHeight={estimatedSize}
|
||||
fixedHeaderContent={() => <>{header}</>}
|
||||
fixedItemHeight={mode === "fixed" ? estimatedSize : undefined}
|
||||
components={{
|
||||
Table: (props) => (
|
||||
<table {...props} style={{ ...style, ...props.style }} />
|
||||
),
|
||||
TableRow: (props) => {
|
||||
return (
|
||||
<Row
|
||||
index={props["data-item-index"]}
|
||||
item={props.item}
|
||||
style={props.style || {}}
|
||||
context={props.context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
// const virtualizer = useVirtualizer({
|
||||
// count: items.length,
|
||||
// estimateSize: () => estimatedSize,
|
||||
// getItemKey,
|
||||
// getScrollElement: () =>
|
||||
// scrollElement || containerRef.current?.closest(".ms-container") || null,
|
||||
// scrollMargin: scrollMargin || containerRef.current?.offsetTop || 0
|
||||
// });
|
||||
|
||||
// if (virtualizerRef) virtualizerRef.current = virtualizer;
|
||||
|
||||
// const virtualItems = virtualizer.getVirtualItems();
|
||||
// return (
|
||||
// <Box
|
||||
// ref={containerRef}
|
||||
// sx={{
|
||||
// height: virtualizer.getTotalSize() + headerSize
|
||||
// }}
|
||||
// >
|
||||
// <table style={style}>
|
||||
// <thead>{header}</thead>
|
||||
// <tbody>
|
||||
// {virtualItems.map((row, index) => (
|
||||
// <Row
|
||||
// key={row.key}
|
||||
// item={items[row.index]}
|
||||
// index={row.index}
|
||||
// rowRef={mode === "dynamic" ? virtualizer.measureElement : null}
|
||||
// context={context}
|
||||
// style={{
|
||||
// height: mode === "dynamic" ? "unset" : `${row.size}px`,
|
||||
// transform: `translateY(${
|
||||
// row.start -
|
||||
// index * row.size -
|
||||
// virtualizer.options.scrollMargin
|
||||
// }px)`
|
||||
// }}
|
||||
// />
|
||||
// ))}
|
||||
// </tbody>
|
||||
// </table>
|
||||
// </Box>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -22,96 +22,104 @@ import Dialog from "../components/dialog";
|
||||
import Field from "../components/field";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { Notebook } from "@notesnook/core";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { store as noteStore } from "../stores/note-store";
|
||||
import { store as notebookStore } from "../stores/notebook-store";
|
||||
import { store as appStore } from "../stores/app-store";
|
||||
import { db } from "../common/db";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type AddNotebookDialogProps = {
|
||||
type AddNotebookDialogProps = BaseDialogProps<boolean> & {
|
||||
parentId?: string;
|
||||
edit?: boolean;
|
||||
notebook?: Notebook;
|
||||
onClose: Perform;
|
||||
};
|
||||
|
||||
function AddNotebookDialog(props: AddNotebookDialogProps) {
|
||||
const { notebook, onClose, parentId } = props;
|
||||
const title = useRef<string>(notebook?.title || "");
|
||||
const description = useRef<string>(notebook?.description || "");
|
||||
export const AddNotebookDialog = DialogManager.register(
|
||||
function AddNotebookDialog(props: AddNotebookDialogProps) {
|
||||
const { notebook, onClose, parentId } = props;
|
||||
const title = useRef<string>(notebook?.title || "");
|
||||
const description = useRef<string>(notebook?.description || "");
|
||||
|
||||
const onSubmit = useCallback(async () => {
|
||||
if (!title.current.trim())
|
||||
return showToast("error", "Notebook title cannot be empty.");
|
||||
const onSubmit = useCallback(async () => {
|
||||
if (!title.current.trim())
|
||||
return showToast("error", "Notebook title cannot be empty.");
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
id: props.notebook?.id,
|
||||
title: title.current,
|
||||
description: description.current
|
||||
});
|
||||
if (parentId) {
|
||||
await db.relations.add(
|
||||
{ type: "notebook", id: parentId },
|
||||
{ type: "notebook", id }
|
||||
);
|
||||
}
|
||||
|
||||
await notebookStore.refresh();
|
||||
await noteStore.refresh();
|
||||
await appStore.refreshNavItems();
|
||||
|
||||
showToast(
|
||||
"success",
|
||||
props.edit
|
||||
? "Notebook edited successfully!"
|
||||
: "Notebook created successfully"
|
||||
);
|
||||
onClose(true);
|
||||
}, [props.notebook?.id, props.edit, onClose, parentId]);
|
||||
return (
|
||||
<Dialog
|
||||
testId="add-notebook-dialog"
|
||||
isOpen={true}
|
||||
title={props.edit ? "Edit Notebook" : "Create a Notebook"}
|
||||
description={
|
||||
props.edit
|
||||
? `You are editing "${notebook?.title}".`
|
||||
: "Notebooks are the best way to organize your notes."
|
||||
const id = await db.notebooks.add({
|
||||
id: props.notebook?.id,
|
||||
title: title.current,
|
||||
description: description.current
|
||||
});
|
||||
if (parentId) {
|
||||
await db.relations.add(
|
||||
{ type: "notebook", id: parentId },
|
||||
{ type: "notebook", id }
|
||||
);
|
||||
}
|
||||
onClose={() => onClose(false)}
|
||||
positiveButton={{
|
||||
text: props.edit ? "Save" : "Create",
|
||||
onClick: onSubmit
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => onClose(false) }}
|
||||
>
|
||||
<Field
|
||||
defaultValue={title.current}
|
||||
data-test-id="title-input"
|
||||
autoFocus
|
||||
required
|
||||
label="Title"
|
||||
name="title"
|
||||
id="title"
|
||||
onChange={(e) => (title.current = e.target.value)}
|
||||
onKeyUp={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
await onSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
data-test-id="description-input"
|
||||
label="Description"
|
||||
name="description"
|
||||
id="description"
|
||||
onChange={(e) => (description.current = e.target.value)}
|
||||
defaultValue={description.current}
|
||||
helpText="Optional"
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddNotebookDialog;
|
||||
await notebookStore.refresh();
|
||||
await noteStore.refresh();
|
||||
await appStore.refreshNavItems();
|
||||
|
||||
showToast(
|
||||
"success",
|
||||
props.edit
|
||||
? "Notebook edited successfully!"
|
||||
: "Notebook created successfully"
|
||||
);
|
||||
onClose(true);
|
||||
}, [props.notebook?.id, props.edit, onClose, parentId]);
|
||||
return (
|
||||
<Dialog
|
||||
testId="add-notebook-dialog"
|
||||
isOpen={true}
|
||||
title={props.edit ? "Edit Notebook" : "Create a Notebook"}
|
||||
description={
|
||||
props.edit
|
||||
? `You are editing "${notebook?.title}".`
|
||||
: "Notebooks are the best way to organize your notes."
|
||||
}
|
||||
onClose={() => onClose(false)}
|
||||
positiveButton={{
|
||||
text: props.edit ? "Save" : "Create",
|
||||
onClick: onSubmit
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => onClose(false) }}
|
||||
>
|
||||
<Field
|
||||
defaultValue={title.current}
|
||||
data-test-id="title-input"
|
||||
autoFocus
|
||||
required
|
||||
label="Title"
|
||||
name="title"
|
||||
id="title"
|
||||
onChange={(e) => (title.current = e.target.value)}
|
||||
onKeyUp={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
await onSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
data-test-id="description-input"
|
||||
label="Description"
|
||||
name="description"
|
||||
id="description"
|
||||
onChange={(e) => (description.current = e.target.value)}
|
||||
defaultValue={description.current}
|
||||
helpText="Optional"
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type EditNotebookDialogProps = { notebookId: string };
|
||||
export const EditNotebookDialog = {
|
||||
show: async (props: EditNotebookDialogProps) => {
|
||||
const notebook = await db.notebooks.notebook(props.notebookId);
|
||||
if (!notebook) return;
|
||||
return AddNotebookDialog.show({ edit: true, notebook });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Dialog from "../components/dialog";
|
||||
import Field from "../components/field";
|
||||
import { Box, Button, Flex, Label, Radio, Text } from "@theme-ui/components";
|
||||
@@ -37,11 +36,11 @@ import { useStore as useThemeStore } from "../stores/theme-store";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { MONTHS_FULL, getTimeFormat } from "@notesnook/core/dist/utils/date";
|
||||
import { Note, Reminder } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
export type AddReminderDialogProps = {
|
||||
onClose: Perform;
|
||||
export type AddReminderDialogProps = BaseDialogProps<boolean> & {
|
||||
reminder?: Reminder;
|
||||
note?: Note;
|
||||
};
|
||||
@@ -116,419 +115,425 @@ const recurringModes = [
|
||||
}
|
||||
];
|
||||
|
||||
export default function AddReminderDialog(props: AddReminderDialogProps) {
|
||||
const { reminder, note } = props;
|
||||
export const AddReminderDialog = DialogManager.register(
|
||||
function AddReminderDialog(props: AddReminderDialogProps) {
|
||||
const { reminder, note } = props;
|
||||
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>(
|
||||
reminder?.selectedDays ?? []
|
||||
);
|
||||
const [recurringMode, setRecurringMode] = useState<
|
||||
ValueOf<typeof RecurringModes>
|
||||
>(reminder?.recurringMode ?? RecurringModes.DAY);
|
||||
const [mode, setMode] = useState<ValueOf<typeof Modes>>(
|
||||
reminder?.mode ?? Modes.ONCE
|
||||
);
|
||||
const [priority, setPriority] = usePersistentState<
|
||||
ValueOf<typeof Priorities>
|
||||
>("reminders:default_priority", reminder?.priority ?? Priorities.VIBRATE);
|
||||
const [date, setDate] = useState(dayjs(reminder?.date));
|
||||
const [title, setTitle] = useState<string>(
|
||||
note?.title ?? reminder?.title ?? ""
|
||||
);
|
||||
const [description, setDescription] = useState<string>(
|
||||
note?.headline ?? reminder?.description ?? ""
|
||||
);
|
||||
const [showCalendar, setShowCalendar] = useState(false);
|
||||
const refresh = useStore((state) => state.refresh);
|
||||
const isUserPremium = useIsUserPremium();
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const dateInputRef = useRef<HTMLInputElement>(null);
|
||||
const [selectedDays, setSelectedDays] = useState<number[]>(
|
||||
reminder?.selectedDays ?? []
|
||||
);
|
||||
const [recurringMode, setRecurringMode] = useState<
|
||||
ValueOf<typeof RecurringModes>
|
||||
>(reminder?.recurringMode ?? RecurringModes.DAY);
|
||||
const [mode, setMode] = useState<ValueOf<typeof Modes>>(
|
||||
reminder?.mode ?? Modes.ONCE
|
||||
);
|
||||
const [priority, setPriority] = usePersistentState<
|
||||
ValueOf<typeof Priorities>
|
||||
>("reminders:default_priority", reminder?.priority ?? Priorities.VIBRATE);
|
||||
const [date, setDate] = useState(dayjs(reminder?.date));
|
||||
const [title, setTitle] = useState<string>(
|
||||
note?.title ?? reminder?.title ?? ""
|
||||
);
|
||||
const [description, setDescription] = useState<string>(
|
||||
note?.headline ?? reminder?.description ?? ""
|
||||
);
|
||||
const [showCalendar, setShowCalendar] = useState(false);
|
||||
const refresh = useStore((state) => state.refresh);
|
||||
const isUserPremium = useIsUserPremium();
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const dateInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const repeatsDaily =
|
||||
(selectedDays.length === 7 && recurringMode === RecurringModes.WEEK) ||
|
||||
(selectedDays.length === 31 && recurringMode === RecurringModes.MONTH) ||
|
||||
recurringMode === RecurringModes.DAY;
|
||||
const repeatsDaily =
|
||||
(selectedDays.length === 7 && recurringMode === RecurringModes.WEEK) ||
|
||||
(selectedDays.length === 31 && recurringMode === RecurringModes.MONTH) ||
|
||||
recurringMode === RecurringModes.DAY;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={reminder ? "Edit reminder" : "Add a reminder"}
|
||||
testId="add-reminder-dialog"
|
||||
onClose={() => props.onClose(false)}
|
||||
sx={{ fontFamily: "body" }}
|
||||
positiveButton={{
|
||||
text: reminder ? "Save" : "Add",
|
||||
disabled:
|
||||
!title ||
|
||||
(mode !== Modes.ONCE &&
|
||||
recurringMode !== RecurringModes.DAY &&
|
||||
recurringMode !== RecurringModes.YEAR &&
|
||||
!selectedDays.length),
|
||||
onClick: async () => {
|
||||
if (!("Notification" in window))
|
||||
showToast(
|
||||
"warn",
|
||||
"Reminders will not be active on this device as it does not support notifications."
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={reminder ? "Edit reminder" : "Add a reminder"}
|
||||
testId="add-reminder-dialog"
|
||||
onClose={() => props.onClose(false)}
|
||||
sx={{ fontFamily: "body" }}
|
||||
positiveButton={{
|
||||
text: reminder ? "Save" : "Add",
|
||||
disabled:
|
||||
!title ||
|
||||
(mode !== Modes.ONCE &&
|
||||
recurringMode !== RecurringModes.DAY &&
|
||||
recurringMode !== RecurringModes.YEAR &&
|
||||
!selectedDays.length),
|
||||
onClick: async () => {
|
||||
if (!("Notification" in window))
|
||||
showToast(
|
||||
"warn",
|
||||
"Reminders will not be active on this device as it does not support notifications."
|
||||
);
|
||||
|
||||
const permissionResult = await Notification.requestPermission();
|
||||
if (!IS_TESTING && permissionResult !== "granted") {
|
||||
showToast(
|
||||
"error",
|
||||
"Please grant notifications permission to add new reminders."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const permissionResult = await Notification.requestPermission();
|
||||
if (!IS_TESTING && permissionResult !== "granted") {
|
||||
showToast(
|
||||
"error",
|
||||
"Please grant notifications permission to add new reminders."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode !== Modes.REPEAT && date.isBefore(dayjs())) {
|
||||
showToast(
|
||||
"error",
|
||||
"Reminder time cannot be earlier than the current time."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (mode !== Modes.REPEAT && date.isBefore(dayjs())) {
|
||||
showToast(
|
||||
"error",
|
||||
"Reminder time cannot be earlier than the current time."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = await db.reminders.add({
|
||||
id: reminder?.id,
|
||||
recurringMode,
|
||||
mode,
|
||||
priority,
|
||||
selectedDays,
|
||||
date: date.valueOf(),
|
||||
title,
|
||||
description,
|
||||
disabled: false,
|
||||
...(date.isAfter(dayjs()) ? { snoozeUntil: 0 } : {})
|
||||
});
|
||||
const id = await db.reminders.add({
|
||||
id: reminder?.id,
|
||||
recurringMode,
|
||||
mode,
|
||||
priority,
|
||||
selectedDays,
|
||||
date: date.valueOf(),
|
||||
title,
|
||||
description,
|
||||
disabled: false,
|
||||
...(date.isAfter(dayjs()) ? { snoozeUntil: 0 } : {})
|
||||
});
|
||||
|
||||
if (id && note) {
|
||||
await db.relations.add(note, { id, type: "reminder" });
|
||||
}
|
||||
if (id && note) {
|
||||
await db.relations.add(note, { id, type: "reminder" });
|
||||
}
|
||||
|
||||
refresh();
|
||||
props.onClose(true);
|
||||
}
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Field
|
||||
id="title"
|
||||
label="Title"
|
||||
required
|
||||
value={title}
|
||||
data-test-id="title-input"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setTitle(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
as="textarea"
|
||||
id="description"
|
||||
label="Description"
|
||||
data-test-id="description-input"
|
||||
helpText="Optional"
|
||||
value={description}
|
||||
styles={{
|
||||
input: {
|
||||
height: 100
|
||||
refresh();
|
||||
props.onClose(true);
|
||||
}
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setDescription(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Flex sx={{ gap: 2, mt: 2 }}>
|
||||
{modes.map((m) => (
|
||||
<Label
|
||||
key={m.id}
|
||||
variant="text.body"
|
||||
data-test-id={`mode-${m.id}`}
|
||||
sx={{
|
||||
width: "auto",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
id="mode"
|
||||
name="mode"
|
||||
defaultChecked={m.id === Modes.ONCE}
|
||||
checked={m.id === mode}
|
||||
disabled={m.premium && !isUserPremium}
|
||||
sx={{ color: m.id === mode ? "accent" : "icon" }}
|
||||
onChange={() => {
|
||||
if (m.premium && !isUserPremium) return;
|
||||
setMode(m.id);
|
||||
setRecurringMode(RecurringModes.DAY);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
/>
|
||||
{m.title}
|
||||
{m.premium && !isUserPremium && (
|
||||
<Pro size={18} color="accent" sx={{ ml: 1 }} />
|
||||
)}
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
{mode === Modes.REPEAT ? (
|
||||
<Flex
|
||||
sx={{
|
||||
mt: 2,
|
||||
bg: "var(--background-secondary)",
|
||||
borderRadius: "default",
|
||||
p: 1,
|
||||
flexDirection: "column"
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Field
|
||||
id="title"
|
||||
label="Title"
|
||||
required
|
||||
value={title}
|
||||
data-test-id="title-input"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setTitle(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
as="textarea"
|
||||
id="description"
|
||||
label="Description"
|
||||
data-test-id="description-input"
|
||||
helpText="Optional"
|
||||
value={description}
|
||||
styles={{
|
||||
input: {
|
||||
height: 100
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ alignItems: "center", gap: 1 }}>
|
||||
{recurringModes.map((mode) => (
|
||||
<Button
|
||||
key={mode.id}
|
||||
variant="secondary"
|
||||
data-test-id={`recurring-mode-${mode.id}`}
|
||||
onClick={() => {
|
||||
setRecurringMode(mode.id);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 100,
|
||||
py: 1,
|
||||
px: 2,
|
||||
flexShrink: 0,
|
||||
bg:
|
||||
mode.id === recurringMode
|
||||
? "background-selected"
|
||||
: "transparent",
|
||||
color:
|
||||
mode.id === recurringMode
|
||||
? "paragraph-selected"
|
||||
: "paragraph"
|
||||
}}
|
||||
>
|
||||
{mode.title}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{recurringModes.map((mode) =>
|
||||
mode.id === recurringMode ? (
|
||||
<Box
|
||||
key={mode.id}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns:
|
||||
mode.id === RecurringModes.WEEK
|
||||
? "1fr 1fr 1fr 1fr 1fr 1fr 1fr"
|
||||
: "1fr 1fr 1fr 1fr 1fr 1fr 1fr",
|
||||
mt: mode.options.length > 0 ? 1 : 0,
|
||||
maxHeight: 150,
|
||||
overflowY: "auto",
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{mode.options.map((day, i) => (
|
||||
<Button
|
||||
key={day}
|
||||
variant="secondary"
|
||||
data-test-id={`day-${day}`}
|
||||
onClick={() => {
|
||||
setSelectedDays((days) => {
|
||||
const clone = days.slice();
|
||||
if (clone.indexOf(day) > -1)
|
||||
clone.splice(clone.indexOf(day), 1);
|
||||
else clone.push(day);
|
||||
return clone;
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
py: 1,
|
||||
px: 2,
|
||||
flexShrink: 0,
|
||||
textAlign: "left",
|
||||
bg: selectedDays.includes(day)
|
||||
? "background-selected"
|
||||
: "transparent",
|
||||
color: selectedDays.includes(day)
|
||||
? "paragraph-selected"
|
||||
: "paragraph"
|
||||
}}
|
||||
>
|
||||
{mode.id === "week" ? WEEK_DAYS[i] : day}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
) : null
|
||||
)}
|
||||
</Flex>
|
||||
) : null}
|
||||
|
||||
<Flex sx={{ gap: 2, overflowX: "auto", mt: 2 }}>
|
||||
{mode === Modes.ONCE ? (
|
||||
<>
|
||||
<Field
|
||||
id="date"
|
||||
label="Date"
|
||||
required
|
||||
inputRef={dateInputRef}
|
||||
data-test-id="date-input"
|
||||
helpText={`${db.settings.getDateFormat()}`}
|
||||
action={{
|
||||
icon: Calendar,
|
||||
onClick() {
|
||||
setShowCalendar(true);
|
||||
}
|
||||
}}
|
||||
validate={(t) =>
|
||||
dayjs(t, db.settings.getDateFormat(), true).isValid()
|
||||
}
|
||||
defaultValue={date.format(db.settings.getDateFormat())}
|
||||
onChange={(e) => setDate((d) => setDateOnly(e.target.value, d))}
|
||||
/>
|
||||
<PopupPresenter
|
||||
isOpen={showCalendar}
|
||||
onClose={() => setShowCalendar(false)}
|
||||
position={{
|
||||
isTargetAbsolute: true,
|
||||
target: dateInputRef.current,
|
||||
location: "top"
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setDescription(e.target.value)
|
||||
}
|
||||
/>
|
||||
<Flex sx={{ gap: 2, mt: 2 }}>
|
||||
{modes.map((m) => (
|
||||
<Label
|
||||
key={m.id}
|
||||
variant="text.body"
|
||||
data-test-id={`mode-${m.id}`}
|
||||
sx={{
|
||||
".rdp": {
|
||||
bg: "background",
|
||||
p: 1,
|
||||
boxShadow: `0px 0px 25px 5px ${
|
||||
theme === "dark" ? "#000000aa" : "#0000004e"
|
||||
}`,
|
||||
borderRadius: "dialog",
|
||||
fontFamily: "body",
|
||||
"--rdp-accent-color": "var(--accent)",
|
||||
"--rdp-background-color": "var(--background)",
|
||||
"--rdp-background-color-dark": "var(--background)",
|
||||
"--rdp-selected-color": "var(--paragraph-selected)",
|
||||
color: "paragraph"
|
||||
},
|
||||
".rdp-caption_label": { fontWeight: "heading" }
|
||||
width: "auto",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<DayPicker
|
||||
mode="single"
|
||||
captionLayout="dropdown-buttons"
|
||||
fromYear={new Date().getFullYear()}
|
||||
toYear={new Date().getFullYear() + 99}
|
||||
modifiers={{
|
||||
disabled: { before: new Date() }
|
||||
}}
|
||||
selected={dayjs(date).toDate()}
|
||||
onSelect={(day) => {
|
||||
if (!day) return;
|
||||
const date = getFormattedDate(day, "date");
|
||||
setDate((d) => setDateOnly(date, d));
|
||||
if (dateInputRef.current) dateInputRef.current.value = date;
|
||||
}}
|
||||
styles={{
|
||||
day: {
|
||||
fontFamily: "inherit",
|
||||
fontSize: 14
|
||||
}
|
||||
<Radio
|
||||
id="mode"
|
||||
name="mode"
|
||||
defaultChecked={m.id === Modes.ONCE}
|
||||
checked={m.id === mode}
|
||||
disabled={m.premium && !isUserPremium}
|
||||
sx={{ color: m.id === mode ? "accent" : "icon" }}
|
||||
onChange={() => {
|
||||
if (m.premium && !isUserPremium) return;
|
||||
setMode(m.id);
|
||||
setRecurringMode(RecurringModes.DAY);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
/>
|
||||
</PopupPresenter>
|
||||
</>
|
||||
) : recurringMode === RecurringModes.YEAR ? (
|
||||
<>
|
||||
<LabeledSelect
|
||||
id="month"
|
||||
label="Month"
|
||||
value={`${dayjs(date).month()}`}
|
||||
options={MONTHS_FULL.map((month, index) => ({
|
||||
value: `${index}`,
|
||||
title: month
|
||||
}))}
|
||||
onSelectionChanged={(month) => {
|
||||
setDate((d) => d.month(parseInt(month)));
|
||||
}}
|
||||
/>
|
||||
<LabeledSelect
|
||||
id="day"
|
||||
label="Day"
|
||||
value={`${dayjs(date).date()}`}
|
||||
options={new Array(dayjs(date).daysInMonth())
|
||||
.fill("0")
|
||||
.map((_, day) => ({
|
||||
value: `${day + 1}`,
|
||||
title: `${day + 1}`
|
||||
}))}
|
||||
onSelectionChanged={(day) => {
|
||||
setDate((d) => d.date(parseInt(day)));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<Field
|
||||
id="time"
|
||||
label="Time"
|
||||
required
|
||||
data-test-id="time-input"
|
||||
helpText={`${
|
||||
db.settings.getTimeFormat() === "12-hour" ? "hh:mm AM/PM" : "hh:mm"
|
||||
}`}
|
||||
validate={(t) => {
|
||||
const format =
|
||||
db.settings.getTimeFormat() === "12-hour" ? "hh:mm a" : "HH:mm";
|
||||
return dayjs(t.toLowerCase(), format, true).isValid();
|
||||
}}
|
||||
defaultValue={date.format(getTimeFormat(db.settings.getTimeFormat()))}
|
||||
onChange={(e) => setDate((d) => setTimeOnly(e.target.value, d))}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex sx={{ gap: 2, mt: 2 }}>
|
||||
{priorities.map((p) => (
|
||||
<Label
|
||||
key={p.id}
|
||||
variant="text.body"
|
||||
data-test-id={`priority-${p.id}`}
|
||||
{m.title}
|
||||
{m.premium && !isUserPremium && (
|
||||
<Pro size={18} color="accent" sx={{ ml: 1 }} />
|
||||
)}
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
{mode === Modes.REPEAT ? (
|
||||
<Flex
|
||||
sx={{
|
||||
width: "auto",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
mt: 2,
|
||||
bg: "var(--background-secondary)",
|
||||
borderRadius: "default",
|
||||
p: 1,
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
id="priority"
|
||||
name="priority"
|
||||
sx={{ color: p.id === priority ? "accent" : "icon" }}
|
||||
defaultChecked={p.id === Priorities.VIBRATE}
|
||||
checked={p.id === priority}
|
||||
onChange={() => setPriority(p.id)}
|
||||
/>
|
||||
{p.title}
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
<Flex sx={{ alignItems: "center", gap: 1 }}>
|
||||
{recurringModes.map((mode) => (
|
||||
<Button
|
||||
key={mode.id}
|
||||
variant="secondary"
|
||||
data-test-id={`recurring-mode-${mode.id}`}
|
||||
onClick={() => {
|
||||
setRecurringMode(mode.id);
|
||||
setSelectedDays([]);
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 100,
|
||||
py: 1,
|
||||
px: 2,
|
||||
flexShrink: 0,
|
||||
bg:
|
||||
mode.id === recurringMode
|
||||
? "background-selected"
|
||||
: "transparent",
|
||||
color:
|
||||
mode.id === recurringMode
|
||||
? "paragraph-selected"
|
||||
: "paragraph"
|
||||
}}
|
||||
>
|
||||
{mode.title}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{recurringModes.map((mode) =>
|
||||
mode.id === recurringMode ? (
|
||||
<Box
|
||||
key={mode.id}
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns:
|
||||
mode.id === RecurringModes.WEEK
|
||||
? "1fr 1fr 1fr 1fr 1fr 1fr 1fr"
|
||||
: "1fr 1fr 1fr 1fr 1fr 1fr 1fr",
|
||||
mt: mode.options.length > 0 ? 1 : 0,
|
||||
maxHeight: 150,
|
||||
overflowY: "auto",
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{mode.options.map((day, i) => (
|
||||
<Button
|
||||
key={day}
|
||||
variant="secondary"
|
||||
data-test-id={`day-${day}`}
|
||||
onClick={() => {
|
||||
setSelectedDays((days) => {
|
||||
const clone = days.slice();
|
||||
if (clone.indexOf(day) > -1)
|
||||
clone.splice(clone.indexOf(day), 1);
|
||||
else clone.push(day);
|
||||
return clone;
|
||||
});
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
py: 1,
|
||||
px: 2,
|
||||
flexShrink: 0,
|
||||
textAlign: "left",
|
||||
bg: selectedDays.includes(day)
|
||||
? "background-selected"
|
||||
: "transparent",
|
||||
color: selectedDays.includes(day)
|
||||
? "paragraph-selected"
|
||||
: "paragraph"
|
||||
}}
|
||||
>
|
||||
{mode.id === "week" ? WEEK_DAYS[i] : day}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
) : null
|
||||
)}
|
||||
</Flex>
|
||||
) : null}
|
||||
|
||||
{mode === Modes.REPEAT ? (
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
{selectedDays.length === 0 && recurringMode !== RecurringModes.DAY
|
||||
? recurringMode === RecurringModes.WEEK
|
||||
? "Select day of the week to repeat the reminder."
|
||||
: "Select nth day(s) of the month to repeat the reminder."
|
||||
: repeatsDaily
|
||||
? `Repeats daily at ${date.format(timeFormat())}.`
|
||||
: `Repeats every ${recurringMode} on ${getSelectedDaysText(
|
||||
selectedDays,
|
||||
recurringMode
|
||||
)} at ${date.format(timeFormat())}.`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
{`The reminder will start on ${date} at ${date.format(
|
||||
timeFormat()
|
||||
)}.`}
|
||||
</Text>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
<Flex sx={{ gap: 2, overflowX: "auto", mt: 2 }}>
|
||||
{mode === Modes.ONCE ? (
|
||||
<>
|
||||
<Field
|
||||
id="date"
|
||||
label="Date"
|
||||
required
|
||||
inputRef={dateInputRef}
|
||||
data-test-id="date-input"
|
||||
helpText={`${db.settings.getDateFormat()}`}
|
||||
action={{
|
||||
icon: Calendar,
|
||||
onClick() {
|
||||
setShowCalendar(true);
|
||||
}
|
||||
}}
|
||||
validate={(t) =>
|
||||
dayjs(t, db.settings.getDateFormat(), true).isValid()
|
||||
}
|
||||
defaultValue={date.format(db.settings.getDateFormat())}
|
||||
onChange={(e) => setDate((d) => setDateOnly(e.target.value, d))}
|
||||
/>
|
||||
<PopupPresenter
|
||||
isOpen={showCalendar}
|
||||
onClose={() => setShowCalendar(false)}
|
||||
position={{
|
||||
isTargetAbsolute: true,
|
||||
target: dateInputRef.current,
|
||||
location: "top"
|
||||
}}
|
||||
sx={{
|
||||
".rdp": {
|
||||
bg: "background",
|
||||
p: 1,
|
||||
boxShadow: `0px 0px 25px 5px ${
|
||||
theme === "dark" ? "#000000aa" : "#0000004e"
|
||||
}`,
|
||||
borderRadius: "dialog",
|
||||
fontFamily: "body",
|
||||
"--rdp-accent-color": "var(--accent)",
|
||||
"--rdp-background-color": "var(--background)",
|
||||
"--rdp-background-color-dark": "var(--background)",
|
||||
"--rdp-selected-color": "var(--paragraph-selected)",
|
||||
color: "paragraph"
|
||||
},
|
||||
".rdp-caption_label": { fontWeight: "heading" }
|
||||
}}
|
||||
>
|
||||
<DayPicker
|
||||
mode="single"
|
||||
captionLayout="dropdown-buttons"
|
||||
fromYear={new Date().getFullYear()}
|
||||
toYear={new Date().getFullYear() + 99}
|
||||
modifiers={{
|
||||
disabled: { before: new Date() }
|
||||
}}
|
||||
selected={dayjs(date).toDate()}
|
||||
onSelect={(day) => {
|
||||
if (!day) return;
|
||||
const date = getFormattedDate(day, "date");
|
||||
setDate((d) => setDateOnly(date, d));
|
||||
if (dateInputRef.current) dateInputRef.current.value = date;
|
||||
}}
|
||||
styles={{
|
||||
day: {
|
||||
fontFamily: "inherit",
|
||||
fontSize: 14
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PopupPresenter>
|
||||
</>
|
||||
) : recurringMode === RecurringModes.YEAR ? (
|
||||
<>
|
||||
<LabeledSelect
|
||||
id="month"
|
||||
label="Month"
|
||||
value={`${dayjs(date).month()}`}
|
||||
options={MONTHS_FULL.map((month, index) => ({
|
||||
value: `${index}`,
|
||||
title: month
|
||||
}))}
|
||||
onSelectionChanged={(month) => {
|
||||
setDate((d) => d.month(parseInt(month)));
|
||||
}}
|
||||
/>
|
||||
<LabeledSelect
|
||||
id="day"
|
||||
label="Day"
|
||||
value={`${dayjs(date).date()}`}
|
||||
options={new Array(dayjs(date).daysInMonth())
|
||||
.fill("0")
|
||||
.map((_, day) => ({
|
||||
value: `${day + 1}`,
|
||||
title: `${day + 1}`
|
||||
}))}
|
||||
onSelectionChanged={(day) => {
|
||||
setDate((d) => d.date(parseInt(day)));
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<Field
|
||||
id="time"
|
||||
label="Time"
|
||||
required
|
||||
data-test-id="time-input"
|
||||
helpText={`${
|
||||
db.settings.getTimeFormat() === "12-hour"
|
||||
? "hh:mm AM/PM"
|
||||
: "hh:mm"
|
||||
}`}
|
||||
validate={(t) => {
|
||||
const format =
|
||||
db.settings.getTimeFormat() === "12-hour" ? "hh:mm a" : "HH:mm";
|
||||
return dayjs(t.toLowerCase(), format, true).isValid();
|
||||
}}
|
||||
defaultValue={date.format(
|
||||
getTimeFormat(db.settings.getTimeFormat())
|
||||
)}
|
||||
onChange={(e) => setDate((d) => setTimeOnly(e.target.value, d))}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex sx={{ gap: 2, mt: 2 }}>
|
||||
{priorities.map((p) => (
|
||||
<Label
|
||||
key={p.id}
|
||||
variant="text.body"
|
||||
data-test-id={`priority-${p.id}`}
|
||||
sx={{
|
||||
width: "auto",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
id="priority"
|
||||
name="priority"
|
||||
sx={{ color: p.id === priority ? "accent" : "icon" }}
|
||||
defaultChecked={p.id === Priorities.VIBRATE}
|
||||
checked={p.id === priority}
|
||||
onChange={() => setPriority(p.id)}
|
||||
/>
|
||||
{p.title}
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
{mode === Modes.REPEAT ? (
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
{selectedDays.length === 0 && recurringMode !== RecurringModes.DAY
|
||||
? recurringMode === RecurringModes.WEEK
|
||||
? "Select day of the week to repeat the reminder."
|
||||
: "Select nth day(s) of the month to repeat the reminder."
|
||||
: repeatsDaily
|
||||
? `Repeats daily at ${date.format(timeFormat())}.`
|
||||
: `Repeats every ${recurringMode} on ${getSelectedDaysText(
|
||||
selectedDays,
|
||||
recurringMode
|
||||
)} at ${date.format(timeFormat())}.`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
{`The reminder will start on ${date} at ${date.format(
|
||||
timeFormat()
|
||||
)}.`}
|
||||
</Text>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function setTimeOnly(str: string, date: dayjs.Dayjs) {
|
||||
const value = dayjs(str, timeFormat(), true);
|
||||
@@ -619,3 +624,12 @@ function LabeledSelect(props: LabeledSelectProps) {
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
type EditReminderDialogProps = { reminderId: string };
|
||||
export const EditReminderDialog = {
|
||||
show: async (props: EditReminderDialogProps) => {
|
||||
const reminder = await db.reminders.reminder(props.reminderId);
|
||||
if (!reminder) return;
|
||||
return AddReminderDialog.show({ reminder });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,12 +29,12 @@ import Dialog from "../components/dialog";
|
||||
import { useStore, store } from "../stores/tag-store";
|
||||
import { store as notestore } from "../stores/note-store";
|
||||
import { useEditorStore } from "../stores/editor-store";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { FilteredList } from "../components/filtered-list";
|
||||
import { ItemReference, Tag } from "@notesnook/core/dist/types";
|
||||
import { create } from "zustand";
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { ResolvedItem } from "@notesnook/common";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type SelectedReference = {
|
||||
id: string;
|
||||
@@ -51,12 +51,10 @@ export const useSelectionStore = create<ISelectionStore>((set) => ({
|
||||
setSelected: (selected) => set({ selected: selected.slice() })
|
||||
}));
|
||||
|
||||
export type AddTagsDialogProps = {
|
||||
onClose: Perform;
|
||||
noteIds: string[];
|
||||
};
|
||||
|
||||
function AddTagsDialog(props: AddTagsDialogProps) {
|
||||
type AddTagsDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
||||
export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
|
||||
props: AddTagsDialogProps
|
||||
) {
|
||||
const { onClose, noteIds } = props;
|
||||
|
||||
const [tags, setTags] = useState<VirtualizedGrouping<Tag> | undefined>(
|
||||
@@ -101,7 +99,6 @@ function AddTagsDialog(props: AddTagsDialogProps) {
|
||||
else await db.relations.unlink(tagRef, noteRef);
|
||||
}
|
||||
}
|
||||
await useEditorStore.getState().refreshTags();
|
||||
await store.get().refresh();
|
||||
await notestore.get().refresh();
|
||||
onClose(true);
|
||||
@@ -150,7 +147,7 @@ function AddTagsDialog(props: AddTagsDialogProps) {
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function TagItem(props: { tag: Tag }) {
|
||||
const { tag } = props;
|
||||
@@ -201,8 +198,6 @@ function TagItem(props: { tag: Tag }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default AddTagsDialog;
|
||||
|
||||
function SelectedCheck({ id, size = 20 }: { id: string; size?: number }) {
|
||||
const selected = useSelectionStore((store) => store.selected);
|
||||
const selectedTag = selected.find((item) => item.id === id);
|
||||
|
||||
@@ -22,31 +22,36 @@ import AnnouncementBody from "../components/announcements/body";
|
||||
import { store as announcementStore } from "../stores/announcement-store";
|
||||
import { useCallback } from "react";
|
||||
import BaseDialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
function AnnouncementDialog(props) {
|
||||
const { announcement, onClose } = props;
|
||||
type AnnouncementDialogProps = BaseDialogProps<boolean> & {
|
||||
announcement: any;
|
||||
};
|
||||
export const AnnouncementDialog = DialogManager.register(
|
||||
function AnnouncementDialog(props: AnnouncementDialogProps) {
|
||||
const { announcement, onClose } = props;
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
announcementStore.get().dismiss(announcement.id);
|
||||
onClose(true);
|
||||
}, [announcement, onClose]);
|
||||
const dismiss = useCallback(() => {
|
||||
announcementStore.get().dismiss(announcement.id);
|
||||
onClose(true);
|
||||
}, [announcement, onClose]);
|
||||
|
||||
return (
|
||||
<BaseDialog isOpen onClose={props.onClose} width={"30%"}>
|
||||
<Flex
|
||||
bg="background"
|
||||
sx={{
|
||||
position: "relative",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<AnnouncementBody
|
||||
dismiss={dismiss}
|
||||
components={announcement.body}
|
||||
type="dialog"
|
||||
/>
|
||||
</Flex>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
export default AnnouncementDialog;
|
||||
return (
|
||||
<BaseDialog isOpen onClose={() => onClose(false)} width={"30%"}>
|
||||
<Flex
|
||||
bg="background"
|
||||
sx={{
|
||||
position: "relative",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<AnnouncementBody
|
||||
dismiss={dismiss}
|
||||
components={announcement.body}
|
||||
type="dialog"
|
||||
/>
|
||||
</Flex>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, memo, useRef } from "react";
|
||||
import { useEffect, useState, memo, useRef, startTransition } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -29,11 +29,12 @@ import {
|
||||
Text
|
||||
} from "@theme-ui/components";
|
||||
import { store, useStore } from "../stores/attachment-store";
|
||||
import { ResolvedItem, formatBytes, usePromise } from "@notesnook/common";
|
||||
import { formatBytes, usePromise, useResolvedItem } from "@notesnook/common";
|
||||
import Dialog from "../components/dialog";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ClearCache,
|
||||
Close,
|
||||
DoubleCheckmark,
|
||||
Download,
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
import NavigationItem from "../components/navigation-menu/navigation-item";
|
||||
import { pluralize } from "@notesnook/common";
|
||||
import { db } from "../common/db";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { Attachment } from "../components/attachment";
|
||||
import { ScopedThemeProvider } from "../components/theme-provider";
|
||||
import {
|
||||
@@ -63,6 +63,10 @@ import {
|
||||
VirtualizedTableRowProps
|
||||
} from "../components/virtualized-table";
|
||||
import { FlexScrollContainer } from "../components/scroll-container";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { ConfirmDialog } from "./confirm";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { Loader } from "../components/loader";
|
||||
|
||||
type ToolbarAction = {
|
||||
title: string;
|
||||
@@ -101,122 +105,114 @@ type SortOptions = {
|
||||
id: "filename" | "size" | "dateUploaded";
|
||||
direction: "asc" | "desc";
|
||||
};
|
||||
type AttachmentsDialogProps = { onClose: Perform };
|
||||
function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
||||
const allAttachments = useStore((store) => store.attachments);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<AttachmentType>>();
|
||||
const [counts, setCounts] = useState<Record<Route, number>>({
|
||||
all: 0,
|
||||
documents: 0,
|
||||
images: 0,
|
||||
orphaned: 0,
|
||||
uploads: 0,
|
||||
videos: 0,
|
||||
audio: 0
|
||||
});
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [sortBy, setSortBy] = useState<SortOptions>({
|
||||
id: "filename",
|
||||
direction: "asc"
|
||||
});
|
||||
const currentRoute = useRef<Route>("all");
|
||||
const refresh = useStore((store) => store.refresh);
|
||||
const download = useStore((store) => store.download);
|
||||
type AttachmentsDialogProps = BaseDialogProps<false>;
|
||||
export const AttachmentsDialog = DialogManager.register(
|
||||
function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
||||
const nonce = useStore((store) => store.nonce);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<AttachmentType>>();
|
||||
const [counts, setCounts] = useState<Record<Route, number>>({
|
||||
all: 0,
|
||||
documents: 0,
|
||||
images: 0,
|
||||
orphaned: 0,
|
||||
uploads: 0,
|
||||
videos: 0,
|
||||
audio: 0
|
||||
});
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [sortBy, setSortBy] = useState<SortOptions>({
|
||||
id: "filename",
|
||||
direction: "asc"
|
||||
});
|
||||
const currentRoute = useRef<Route>("all");
|
||||
const download = useStore((store) => store.download);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setAttachments(
|
||||
await filterAttachments(currentRoute.current).sorted({
|
||||
useEffect(() => {
|
||||
filterAttachments(currentRoute.current)
|
||||
.sorted({
|
||||
sortBy: sortBy.id,
|
||||
sortDirection: sortBy.direction
|
||||
})
|
||||
);
|
||||
})();
|
||||
}, [sortBy, allAttachments]);
|
||||
.then((value) => startTransition(() => setAttachments(value)));
|
||||
}, [sortBy, nonce]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
setCounts(await getCounts());
|
||||
})();
|
||||
}, [allAttachments]);
|
||||
useEffect(() => {
|
||||
getCounts().then((counts) => startTransition(() => setCounts(counts)));
|
||||
}, [nonce]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
width={"70%"}
|
||||
onClose={() => onClose(false)}
|
||||
noScroll
|
||||
sx={{ bg: "transparent" }}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
height: "80vw",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
width={"70%"}
|
||||
onClose={() => onClose(false)}
|
||||
noScroll
|
||||
sx={{ bg: "transparent" }}
|
||||
>
|
||||
<Sidebar
|
||||
onDownloadAll={async () =>
|
||||
download(
|
||||
await db.attachments.all.ids({
|
||||
sortBy: "dateCreated",
|
||||
sortDirection: "desc"
|
||||
})
|
||||
)
|
||||
}
|
||||
filter={async (query) => {
|
||||
setAttachments(await db.lookup.attachments(query).sorted());
|
||||
}}
|
||||
counts={counts}
|
||||
onRouteChange={async (route) => {
|
||||
currentRoute.current = route;
|
||||
setSelected([]);
|
||||
setAttachments(
|
||||
await filterAttachments(currentRoute.current).sorted({
|
||||
sortBy: sortBy.id,
|
||||
sortDirection: sortBy.direction
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
variant="columnFill"
|
||||
sx={{
|
||||
bg: "background",
|
||||
flexDirection: "column",
|
||||
pl: 4,
|
||||
pt: 2,
|
||||
height: "80vw",
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<FlexScrollContainer>
|
||||
<Flex sx={{ justifyContent: "space-between" }}>
|
||||
<Flex sx={{ gap: 1 }}>
|
||||
{TOOLBAR_ACTIONS.map((tool) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={tool.title}
|
||||
title={tool.title}
|
||||
onClick={() =>
|
||||
tool.onClick({
|
||||
selected
|
||||
// : attachments.filter(
|
||||
// (a) => selected.indexOf(a.id) > -1
|
||||
// )
|
||||
})
|
||||
}
|
||||
disabled={!selected.length}
|
||||
sx={{ bg: "transparent", p: 1 }}
|
||||
>
|
||||
<tool.icon size={18} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{/* <Button
|
||||
<Sidebar
|
||||
onDownloadAll={async () =>
|
||||
download(
|
||||
await db.attachments.all.ids({
|
||||
sortBy: "dateCreated",
|
||||
sortDirection: "desc"
|
||||
})
|
||||
)
|
||||
}
|
||||
filter={async (query) => {
|
||||
setAttachments(await db.lookup.attachments(query).sorted());
|
||||
}}
|
||||
counts={counts}
|
||||
onRouteChange={async (route) => {
|
||||
currentRoute.current = route;
|
||||
setSelected([]);
|
||||
setAttachments(
|
||||
await filterAttachments(currentRoute.current).sorted({
|
||||
sortBy: sortBy.id,
|
||||
sortDirection: sortBy.direction
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
variant="columnFill"
|
||||
sx={{
|
||||
bg: "background",
|
||||
flexDirection: "column",
|
||||
pl: 4,
|
||||
pt: 2,
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
<FlexScrollContainer>
|
||||
<Flex sx={{ justifyContent: "space-between" }}>
|
||||
<Flex sx={{ gap: 1 }}>
|
||||
{TOOLBAR_ACTIONS.map((tool) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={tool.title}
|
||||
title={tool.title}
|
||||
onClick={() =>
|
||||
tool.onClick({
|
||||
selected
|
||||
// : attachments.filter(
|
||||
// (a) => selected.indexOf(a.id) > -1
|
||||
// )
|
||||
})
|
||||
}
|
||||
disabled={!selected.length}
|
||||
sx={{ bg: "transparent", p: 1 }}
|
||||
>
|
||||
<tool.icon size={18} />
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{/* <Button
|
||||
variant="tool"
|
||||
sx={{ p: 1, display: "flex" }}
|
||||
onClick={async () => {
|
||||
@@ -229,123 +225,127 @@ function AttachmentsDialog({ onClose }: AttachmentsDialogProps) {
|
||||
Upload
|
||||
</Text>
|
||||
</Button> */}
|
||||
</Flex>
|
||||
{attachments && (
|
||||
<VirtualizedTable
|
||||
style={{ tableLayout: "fixed", borderCollapse: "collapse" }}
|
||||
header={
|
||||
<Box
|
||||
as="tr"
|
||||
sx={{
|
||||
height: 40,
|
||||
th: { borderBottom: "1px solid var(--separator)" },
|
||||
bg: "background"
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="th"
|
||||
variant="body"
|
||||
</Flex>
|
||||
{attachments ? (
|
||||
<VirtualizedTable
|
||||
style={{
|
||||
tableLayout: "fixed",
|
||||
width: "100%",
|
||||
borderCollapse: "collapse"
|
||||
}}
|
||||
header={
|
||||
<Box
|
||||
as="tr"
|
||||
sx={{
|
||||
width: 24,
|
||||
textAlign: "left",
|
||||
fontWeight: "normal",
|
||||
mb: 2
|
||||
height: 40,
|
||||
th: { borderBottom: "1px solid var(--separator)" },
|
||||
bg: "background"
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
<Checkbox
|
||||
sx={{ width: 18, height: 18 }}
|
||||
onChange={async (e) => {
|
||||
setSelected(
|
||||
e.currentTarget.checked
|
||||
? await filterAttachments(
|
||||
currentRoute.current
|
||||
).ids()
|
||||
: []
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</Text>
|
||||
{COLUMNS.map((column) =>
|
||||
!column.title ? (
|
||||
<th key={column.id} />
|
||||
) : (
|
||||
<Box
|
||||
as="th"
|
||||
key={column.id}
|
||||
sx={{
|
||||
width: column.width,
|
||||
cursor: "pointer",
|
||||
px: 1,
|
||||
mb: 2,
|
||||
":hover": { bg: "hover" }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSortBy((sortBy) => ({
|
||||
direction:
|
||||
sortBy.id === column.id &&
|
||||
sortBy.direction === "asc"
|
||||
? "desc"
|
||||
: "asc",
|
||||
id: column.id
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
<Text
|
||||
as="th"
|
||||
variant="body"
|
||||
sx={{
|
||||
width: 24,
|
||||
textAlign: "left",
|
||||
fontWeight: "normal",
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
<Checkbox
|
||||
sx={{ width: 18, height: 18 }}
|
||||
onChange={async (e) => {
|
||||
setSelected(
|
||||
e.currentTarget.checked
|
||||
? await filterAttachments(
|
||||
currentRoute.current
|
||||
).ids()
|
||||
: []
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
</Text>
|
||||
{COLUMNS.map((column) =>
|
||||
!column.title ? (
|
||||
<th key={column.id} />
|
||||
) : (
|
||||
<Box
|
||||
as="th"
|
||||
key={column.id}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
width: column.width,
|
||||
cursor: "pointer",
|
||||
px: 1,
|
||||
mb: 2,
|
||||
":hover": { bg: "hover" }
|
||||
}}
|
||||
onClick={() => {
|
||||
setSortBy((sortBy) => ({
|
||||
direction:
|
||||
sortBy.id === column.id &&
|
||||
sortBy.direction === "asc"
|
||||
? "desc"
|
||||
: "asc",
|
||||
id: column.id
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ textAlign: "left", fontWeight: "normal" }}
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
{column.title}
|
||||
</Text>
|
||||
{sortBy.id === column.id ? (
|
||||
sortBy.direction === "asc" ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)
|
||||
) : null}
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
mode="fixed"
|
||||
estimatedSize={30}
|
||||
headerSize={40}
|
||||
getItemKey={(index) => attachments.key(index)}
|
||||
items={attachments.placeholders}
|
||||
context={{
|
||||
isSelected: (id: string) => selected.indexOf(id) > -1,
|
||||
select: (id: string) => {
|
||||
setSelected((s) => {
|
||||
const copy = s.slice();
|
||||
const index = copy.indexOf(id);
|
||||
if (index > -1) copy.splice(index, 1);
|
||||
else copy.push(id);
|
||||
return copy;
|
||||
});
|
||||
},
|
||||
attachments
|
||||
}}
|
||||
renderRow={AttachmentRow}
|
||||
/>
|
||||
)}
|
||||
</FlexScrollContainer>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ textAlign: "left", fontWeight: "normal" }}
|
||||
>
|
||||
{column.title}
|
||||
</Text>
|
||||
{sortBy.id === column.id ? (
|
||||
sortBy.direction === "asc" ? (
|
||||
<ChevronUp size={16} />
|
||||
) : (
|
||||
<ChevronDown size={16} />
|
||||
)
|
||||
) : null}
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
mode="fixed"
|
||||
estimatedSize={30}
|
||||
getItemKey={(index) => attachments.key(index)}
|
||||
items={attachments.placeholders}
|
||||
context={{
|
||||
isSelected: (id: string) => selected.indexOf(id) > -1,
|
||||
select: (id: string) => {
|
||||
setSelected((s) => {
|
||||
const copy = s.slice();
|
||||
const index = copy.indexOf(id);
|
||||
if (index > -1) copy.splice(index, 1);
|
||||
else copy.push(id);
|
||||
return copy;
|
||||
});
|
||||
},
|
||||
attachments
|
||||
}}
|
||||
renderRow={AttachmentRow}
|
||||
/>
|
||||
) : (
|
||||
<Loader title="Loading attachments..." />
|
||||
)}
|
||||
</FlexScrollContainer>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default AttachmentsDialog;
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function AttachmentRow(
|
||||
props: VirtualizedTableRowProps<
|
||||
@@ -357,23 +357,21 @@ function AttachmentRow(
|
||||
}
|
||||
>
|
||||
) {
|
||||
if (!props.context) return null;
|
||||
const item = useResolvedItem({
|
||||
index: props.index,
|
||||
items: props.context!.attachments,
|
||||
type: "attachment"
|
||||
});
|
||||
if (!item) return null;
|
||||
return (
|
||||
<ResolvedItem
|
||||
index={props.index}
|
||||
items={props.context.attachments}
|
||||
type="attachment"
|
||||
>
|
||||
{({ item }) => (
|
||||
<Attachment
|
||||
rowRef={props.rowRef}
|
||||
style={props.style}
|
||||
item={item}
|
||||
isSelected={props.context?.isSelected(item.id)}
|
||||
onSelected={() => props.context?.select(item.id)}
|
||||
/>
|
||||
)}
|
||||
</ResolvedItem>
|
||||
<Attachment
|
||||
key={item.item.id}
|
||||
rowRef={props.rowRef}
|
||||
style={props.style}
|
||||
item={item?.item}
|
||||
isSelected={props.context?.isSelected(item.item.id)}
|
||||
onSelected={() => props.context?.select(item.item.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -443,6 +441,7 @@ const Sidebar = memo(
|
||||
<Flex
|
||||
className="theme-scope-navigationMenu"
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
width: 240,
|
||||
@@ -484,40 +483,83 @@ const Sidebar = memo(
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{
|
||||
bg: "transparent",
|
||||
borderRadius: 100,
|
||||
position: "relative",
|
||||
width: 38,
|
||||
height: 38
|
||||
}}
|
||||
title="Download all attachments"
|
||||
onClick={async () => {
|
||||
if (downloadStatus) {
|
||||
await cancelDownload();
|
||||
} else {
|
||||
onDownloadAll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{downloadStatus ? <Close size={18} /> : <Download size={18} />}
|
||||
{downloadStatus ? (
|
||||
<Donut
|
||||
value={
|
||||
(downloadStatus.current / downloadStatus.total) * 100
|
||||
<Flex>
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{
|
||||
bg: "transparent",
|
||||
borderRadius: 100,
|
||||
position: "relative",
|
||||
width: 38,
|
||||
height: 38
|
||||
}}
|
||||
disabled={!!downloadStatus}
|
||||
title="Clear cache"
|
||||
onClick={async () => {
|
||||
if (
|
||||
await ConfirmDialog.show({
|
||||
title: "Clear attachments cache?",
|
||||
message: `Clearing attachments cache will perform the following actions:
|
||||
|
||||
- Downloaded images & files: **cleared**
|
||||
- Pending uploads: **cleared**
|
||||
- Uploaded images & files: _unaffected_
|
||||
|
||||
All attachments will be downloaded & cached again on access.
|
||||
|
||||
---
|
||||
|
||||
**Only use this for troubleshooting purposes. If you are having persistent issues, it is recommended that you reach out to us via support@streetwriters.co so we can help you resolve it permanently.**`,
|
||||
negativeButtonText: "No",
|
||||
positiveButtonText: "Yes"
|
||||
})
|
||||
) {
|
||||
await db.fs().clear();
|
||||
showToast("success", "Attachments cache cleared!");
|
||||
}
|
||||
max={100}
|
||||
size={38}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
}}
|
||||
>
|
||||
<ClearCache />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
sx={{
|
||||
bg: "transparent",
|
||||
borderRadius: 100,
|
||||
position: "relative",
|
||||
width: 38,
|
||||
height: 38
|
||||
}}
|
||||
title="Download all attachments"
|
||||
onClick={async () => {
|
||||
if (downloadStatus) {
|
||||
await cancelDownload();
|
||||
} else {
|
||||
onDownloadAll();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{downloadStatus ? (
|
||||
<Close size={18} />
|
||||
) : (
|
||||
<Download size={18} />
|
||||
)}
|
||||
{downloadStatus ? (
|
||||
<Donut
|
||||
value={
|
||||
(downloadStatus.current / downloadStatus.total) * 100
|
||||
}
|
||||
max={100}
|
||||
size={38}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -18,105 +18,106 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Field from "../components/field";
|
||||
import Dialog from "../components/dialog";
|
||||
import { Box, Button, Text } from "@theme-ui/components";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
export type PromptDialogProps = {
|
||||
onClose: Perform;
|
||||
export type BackupPasswordDialogProps = BaseDialogProps<boolean> & {
|
||||
validate: (outputs: {
|
||||
password?: string;
|
||||
key?: string;
|
||||
}) => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
export default function BackupPasswordDialog(props: PromptDialogProps) {
|
||||
const { onClose, validate } = props;
|
||||
export const BackupPasswordDialog = DialogManager.register(
|
||||
function BackupPasswordDialog(props: BackupPasswordDialogProps) {
|
||||
const { onClose, validate } = props;
|
||||
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEncryptionKey, setIsEncryptionKey] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEncryptionKey, setIsEncryptionKey] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
testId="password-dialog"
|
||||
title={"Encrypted backup"}
|
||||
description={
|
||||
"Please enter the password to decrypt and restore this backup."
|
||||
}
|
||||
onClose={() => onClose(false)}
|
||||
positiveButton={{
|
||||
form: "backupPasswordForm",
|
||||
type: "submit",
|
||||
loading: isLoading,
|
||||
disabled: isLoading,
|
||||
text: "Restore"
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => onClose(false) }}
|
||||
>
|
||||
<Box
|
||||
id="backupPasswordForm"
|
||||
as="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const key = formData.get("key") as string;
|
||||
const password = formData.get("password") as string;
|
||||
if (!key && !password) return;
|
||||
|
||||
if (await validate({ key, password })) {
|
||||
onClose(true);
|
||||
} else {
|
||||
setError("Wrong password.");
|
||||
}
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
testId="password-dialog"
|
||||
title={"Encrypted backup"}
|
||||
description={
|
||||
"Please enter the password to decrypt and restore this backup."
|
||||
}
|
||||
onClose={() => onClose(false)}
|
||||
positiveButton={{
|
||||
form: "backupPasswordForm",
|
||||
type: "submit",
|
||||
loading: isLoading,
|
||||
disabled: isLoading,
|
||||
text: "Restore"
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => onClose(false) }}
|
||||
>
|
||||
{isEncryptionKey ? (
|
||||
<Field
|
||||
required
|
||||
autoFocus
|
||||
data-test-id="encryption-key"
|
||||
label="Encryption key"
|
||||
type="password"
|
||||
id="key"
|
||||
name="key"
|
||||
/>
|
||||
) : (
|
||||
<Field
|
||||
required
|
||||
autoFocus
|
||||
data-test-id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Button variant="anchor" onClick={() => setIsEncryptionKey((s) => !s)}>
|
||||
{isEncryptionKey
|
||||
? "Don't have encryption key? Use password."
|
||||
: "Forgot password? Use encryption key."}
|
||||
</Button>
|
||||
<Box
|
||||
id="backupPasswordForm"
|
||||
as="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
|
||||
{error && (
|
||||
<Text as="div" mt={1} variant={"error"}>
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const key = formData.get("key") as string;
|
||||
const password = formData.get("password") as string;
|
||||
if (!key && !password) return;
|
||||
|
||||
if (await validate({ key, password })) {
|
||||
onClose(true);
|
||||
} else {
|
||||
setError("Wrong password.");
|
||||
}
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isEncryptionKey ? (
|
||||
<Field
|
||||
required
|
||||
autoFocus
|
||||
data-test-id="encryption-key"
|
||||
label="Encryption key"
|
||||
type="password"
|
||||
id="key"
|
||||
name="key"
|
||||
/>
|
||||
) : (
|
||||
<Field
|
||||
required
|
||||
autoFocus
|
||||
data-test-id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
id="password"
|
||||
name="password"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Button variant="anchor" onClick={() => setIsEncryptionKey((s) => !s)}>
|
||||
{isEncryptionKey
|
||||
? "Don't have encryption key? Use password."
|
||||
: "Forgot password? Use encryption key."}
|
||||
</Button>
|
||||
|
||||
{error && (
|
||||
<Text as="div" mt={1} variant={"error"}>
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -45,14 +45,16 @@ import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
||||
import BaseDialog from "../../components/dialog";
|
||||
import { ScopedThemeProvider } from "../../components/theme-provider";
|
||||
import { User } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
|
||||
|
||||
type BuyDialogProps = {
|
||||
type BuyDialogProps = BaseDialogProps<false> & {
|
||||
couponCode?: string;
|
||||
plan?: "monthly" | "yearly" | "education";
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function BuyDialog(props: BuyDialogProps) {
|
||||
export const BuyDialog = DialogManager.register(function BuyDialog(
|
||||
props: BuyDialogProps
|
||||
) {
|
||||
const { onClose, couponCode, plan } = props;
|
||||
const theme = useTheme() as Theme;
|
||||
|
||||
@@ -73,7 +75,7 @@ export function BuyDialog(props: BuyDialogProps) {
|
||||
<BaseDialog
|
||||
isOpen={true}
|
||||
width={"868px"}
|
||||
onClose={() => props.onClose()}
|
||||
onClose={() => props.onClose(false)}
|
||||
noScroll
|
||||
sx={{
|
||||
bg: "transparent",
|
||||
@@ -107,13 +109,13 @@ export function BuyDialog(props: BuyDialogProps) {
|
||||
py: 50
|
||||
}}
|
||||
>
|
||||
<SideBar onClose={onClose} initialPlan={plan} />
|
||||
<SideBar onClose={() => onClose(false)} initialPlan={plan} />
|
||||
</ScopedThemeProvider>
|
||||
<Details />
|
||||
</Flex>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
type SideBarProps = {
|
||||
initialPlan?: Period;
|
||||
|
||||
@@ -17,4 +17,4 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export { BuyDialog as default } from "./buy-dialog";
|
||||
export * from "./buy-dialog";
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Plan, PricingInfo } from "./types";
|
||||
import { create } from "zustand";
|
||||
import { produce } from "immer";
|
||||
import { create as produce } from "mutative";
|
||||
|
||||
interface ICheckoutStore {
|
||||
isCompleted: boolean;
|
||||
|
||||
@@ -19,15 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Box, Checkbox, Label, Text } from "@theme-ui/components";
|
||||
import { useRef } from "react";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { mdToHtml } from "../utils/md";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { db } from "../common/db";
|
||||
import { AppVersion, getChangelog } from "../utils/version";
|
||||
import { downloadUpdate } from "../utils/updater";
|
||||
|
||||
type Check = { text: string; default?: boolean };
|
||||
export type ConfirmDialogProps<TCheckId extends string> = {
|
||||
export type ConfirmDialogProps<TCheckId extends string> = BaseDialogProps<
|
||||
false | Record<TCheckId, boolean>
|
||||
> & {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onClose: Perform<false | Record<TCheckId, boolean>>;
|
||||
width?: number;
|
||||
positiveButtonText?: string;
|
||||
negativeButtonText?: string;
|
||||
@@ -35,9 +39,9 @@ export type ConfirmDialogProps<TCheckId extends string> = {
|
||||
checks?: Partial<Record<TCheckId, Check>>;
|
||||
};
|
||||
|
||||
function ConfirmDialog<TCheckId extends string>(
|
||||
props: ConfirmDialogProps<TCheckId>
|
||||
) {
|
||||
export const ConfirmDialog = DialogManager.register(function ConfirmDialog<
|
||||
TCheckId extends string
|
||||
>(props: ConfirmDialogProps<TCheckId>) {
|
||||
const {
|
||||
onClose,
|
||||
title,
|
||||
@@ -116,6 +120,85 @@ function ConfirmDialog<TCheckId extends string>(
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
export function showMultiDeleteConfirmation(length: number) {
|
||||
return ConfirmDialog.show({
|
||||
title: `Delete ${length} items?`,
|
||||
message: `These items will be **kept in your Trash for ${
|
||||
db.settings.getTrashCleanupInterval() || 7
|
||||
} days** after which they will be permanently deleted.`,
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export default ConfirmDialog;
|
||||
export function showMultiPermanentDeleteConfirmation(length: number) {
|
||||
return ConfirmDialog.show({
|
||||
title: `Permanently delete ${length} items?`,
|
||||
message:
|
||||
"These items will be **permanently deleted**. This is IRREVERSIBLE.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export function showLogoutConfirmation() {
|
||||
return ConfirmDialog.show({
|
||||
title: `Logout?`,
|
||||
message:
|
||||
"Logging out will clear all data stored on THIS DEVICE. Make sure you have synced all your changes before logging out.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export function showClearSessionsConfirmation() {
|
||||
return ConfirmDialog.show({
|
||||
title: `Logout from other devices?`,
|
||||
message:
|
||||
"All other logged-in devices will be forced to logout stopping sync. Use with care lest you lose important notes.",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
}
|
||||
|
||||
export async function showUpdateAvailableNotice({
|
||||
version
|
||||
}: {
|
||||
version: string;
|
||||
}) {
|
||||
const changelog = await getChangelog(version);
|
||||
|
||||
return showUpdateDialog({
|
||||
title: `New version available`,
|
||||
subtitle: `v${version} is available for download`,
|
||||
changelog,
|
||||
action: { text: `Update now`, onClick: () => downloadUpdate() }
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateDialogProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
changelog: string;
|
||||
action: {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
};
|
||||
async function showUpdateDialog({
|
||||
title,
|
||||
subtitle,
|
||||
changelog,
|
||||
action
|
||||
}: UpdateDialogProps) {
|
||||
const result = await ConfirmDialog.show({
|
||||
title,
|
||||
subtitle,
|
||||
message: changelog,
|
||||
width: 500,
|
||||
positiveButtonText: action.text
|
||||
});
|
||||
if (result && action.onClick) action.onClick();
|
||||
}
|
||||
|
||||
@@ -20,91 +20,91 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Box, Flex } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import Field from "../components/field";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { useRef } from "react";
|
||||
import tinycolor from "tinycolor2";
|
||||
import { db } from "../common/db";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type CreateColorDialogProps = {
|
||||
onClose: Perform;
|
||||
onDone: Perform<string>;
|
||||
};
|
||||
function CreateColorDialog(props: CreateColorDialogProps) {
|
||||
const colorRef = useRef<HTMLInputElement>(null);
|
||||
const colorPickerRef = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<Dialog
|
||||
testId="new-color-dialog"
|
||||
isOpen={true}
|
||||
title={"Create a new color"}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
form: "colorForm",
|
||||
type: "submit",
|
||||
text: "Create"
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Box
|
||||
as="form"
|
||||
id="colorForm"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const form = Object.fromEntries(
|
||||
new FormData(e.target as HTMLFormElement).entries()
|
||||
) as { color: string; title: string };
|
||||
if (!tinycolor(form.color, { format: "hex" }).isValid()) {
|
||||
showToast("error", "Please enter a valid hex color (e.g. #ffffff)");
|
||||
return;
|
||||
}
|
||||
const colorId = await db.colors.add({
|
||||
colorCode: form.color,
|
||||
title: form.title
|
||||
});
|
||||
props.onDone(colorId);
|
||||
type CreateColorDialogProps = BaseDialogProps<string | false>;
|
||||
export const CreateColorDialog = DialogManager.register(
|
||||
function CreateColorDialog(props: CreateColorDialogProps) {
|
||||
const colorRef = useRef<HTMLInputElement>(null);
|
||||
const colorPickerRef = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<Dialog
|
||||
testId="new-color-dialog"
|
||||
isOpen={true}
|
||||
title={"Create a new color"}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
form: "colorForm",
|
||||
type: "submit",
|
||||
text: "Create"
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Field
|
||||
required
|
||||
label="Title"
|
||||
id="title"
|
||||
name="title"
|
||||
autoFocus
|
||||
data-test-id="title-input"
|
||||
/>
|
||||
<Flex sx={{ alignItems: "end" }}>
|
||||
<Box
|
||||
as="form"
|
||||
id="colorForm"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const form = Object.fromEntries(
|
||||
new FormData(e.target as HTMLFormElement).entries()
|
||||
) as { color: string; title: string };
|
||||
if (!tinycolor(form.color, { format: "hex" }).isValid()) {
|
||||
showToast(
|
||||
"error",
|
||||
"Please enter a valid hex color (e.g. #ffffff)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const colorId = await db.colors.add({
|
||||
colorCode: form.color,
|
||||
title: form.title
|
||||
});
|
||||
props.onClose(colorId);
|
||||
}}
|
||||
>
|
||||
<Field
|
||||
inputRef={colorRef}
|
||||
required
|
||||
label="Color"
|
||||
id="color"
|
||||
name="color"
|
||||
data-test-id="color-input"
|
||||
sx={{ flex: 1 }}
|
||||
onChange={(e) => {
|
||||
const color = tinycolor(e.target.value);
|
||||
if (colorPickerRef.current && color.isValid())
|
||||
colorPickerRef.current.value = color.toHexString();
|
||||
}}
|
||||
label="Title"
|
||||
id="title"
|
||||
name="title"
|
||||
autoFocus
|
||||
data-test-id="title-input"
|
||||
/>
|
||||
<input
|
||||
ref={colorPickerRef}
|
||||
type="color"
|
||||
style={{
|
||||
height: 41,
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 5
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (colorRef.current) colorRef.current.value = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateColorDialog;
|
||||
<Flex sx={{ alignItems: "end" }}>
|
||||
<Field
|
||||
inputRef={colorRef}
|
||||
required
|
||||
label="Color"
|
||||
id="color"
|
||||
name="color"
|
||||
data-test-id="color-input"
|
||||
sx={{ flex: 1 }}
|
||||
onChange={(e) => {
|
||||
const color = tinycolor(e.target.value);
|
||||
if (colorPickerRef.current && color.isValid())
|
||||
colorPickerRef.current.value = color.toHexString();
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={colorPickerRef}
|
||||
type="color"
|
||||
style={{
|
||||
height: 41,
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 5
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (colorRef.current) colorRef.current.value = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Dialog from "../components/dialog";
|
||||
import { Profile } from "@notesnook/core";
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
@@ -28,139 +27,139 @@ import { showFilePicker } from "../utils/file-picker";
|
||||
import { db } from "../common/db";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { useStore as useSettingStore } from "../stores/setting-store";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
export type EditProfilePictureDialogProps = {
|
||||
onClose: Perform;
|
||||
export type EditProfilePictureDialogProps = BaseDialogProps<boolean> & {
|
||||
profile?: Profile;
|
||||
};
|
||||
|
||||
export default function EditProfilePictureDialog(
|
||||
props: EditProfilePictureDialogProps
|
||||
) {
|
||||
const { profile } = props;
|
||||
const [profilePicture, setProfilePicture] = useState<
|
||||
File | string | undefined
|
||||
>(profile?.profilePicture);
|
||||
const profileRef = useRef<AvatarEditor>(null);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [clearProfilePicture, setClearProfilePicture] = useState(false);
|
||||
export const EditProfilePictureDialog = DialogManager.register(
|
||||
function EditProfilePictureDialog(props: EditProfilePictureDialogProps) {
|
||||
const { profile } = props;
|
||||
const [profilePicture, setProfilePicture] = useState<
|
||||
File | string | undefined
|
||||
>(profile?.profilePicture);
|
||||
const profileRef = useRef<AvatarEditor>(null);
|
||||
const [scale, setScale] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [clearProfilePicture, setClearProfilePicture] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Edit profile picture"}
|
||||
description="Your profile data is stored 100% end-to-end encrypted."
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
loading: isLoading,
|
||||
disabled: isLoading,
|
||||
text: "Save",
|
||||
onClick: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const pic = profileRef.current
|
||||
? profileRef.current
|
||||
.getImageScaledToCanvas()
|
||||
.toDataURL("image/jpeg", 0.8)
|
||||
: clearProfilePicture
|
||||
? undefined
|
||||
: profile?.profilePicture;
|
||||
await db.settings.setProfile({
|
||||
profilePicture: pic
|
||||
});
|
||||
await useSettingStore.getState().refresh();
|
||||
showToast("success", "Profile updated!");
|
||||
props.onClose(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast("error", (e as Error).message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
{profilePicture ? (
|
||||
<AvatarEditor
|
||||
ref={profileRef}
|
||||
image={profilePicture}
|
||||
width={256}
|
||||
height={256}
|
||||
border={1}
|
||||
color={[255, 255, 255]}
|
||||
borderRadius={256}
|
||||
scale={scale}
|
||||
style={{
|
||||
width: 150,
|
||||
height: 150,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
sx={{
|
||||
bg: "shade",
|
||||
mr: 2,
|
||||
size: 150,
|
||||
borderRadius: 200,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<User size={60} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex sx={{ gap: 1, alignItems: "center", mt: 2 }}>
|
||||
<Button
|
||||
sx={{ flex: 1 }}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const [file] = await showFilePicker({
|
||||
acceptedFileTypes: "image/*"
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Edit profile picture"}
|
||||
description="Your profile data is stored 100% end-to-end encrypted."
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
loading: isLoading,
|
||||
disabled: isLoading,
|
||||
text: "Save",
|
||||
onClick: async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const pic = profileRef.current
|
||||
? profileRef.current
|
||||
.getImageScaledToCanvas()
|
||||
.toDataURL("image/jpeg", 0.8)
|
||||
: clearProfilePicture
|
||||
? undefined
|
||||
: profile?.profilePicture;
|
||||
await db.settings.setProfile({
|
||||
profilePicture: pic
|
||||
});
|
||||
if (!file) return;
|
||||
setProfilePicture(file);
|
||||
setScale(1);
|
||||
}}
|
||||
>
|
||||
{profilePicture
|
||||
? "Change profile picture"
|
||||
: "Select profile picture"}
|
||||
</Button>
|
||||
await useSettingStore.getState().refresh();
|
||||
showToast("success", "Profile updated!");
|
||||
props.onClose(true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast("error", (e as Error).message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
{profilePicture ? (
|
||||
<AvatarEditor
|
||||
ref={profileRef}
|
||||
image={profilePicture}
|
||||
width={256}
|
||||
height={256}
|
||||
border={1}
|
||||
color={[255, 255, 255]}
|
||||
borderRadius={256}
|
||||
scale={scale}
|
||||
style={{
|
||||
width: 150,
|
||||
height: 150,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
variant="columnCenter"
|
||||
sx={{
|
||||
bg: "shade",
|
||||
mr: 2,
|
||||
size: 150,
|
||||
borderRadius: 200,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<User size={60} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex sx={{ gap: 1, alignItems: "center", mt: 2 }}>
|
||||
<Button
|
||||
sx={{ flex: 1 }}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
if (typeof profilePicture === "string") {
|
||||
setClearProfilePicture(true);
|
||||
setProfilePicture(undefined);
|
||||
} else setProfilePicture(profile?.profilePicture);
|
||||
const [file] = await showFilePicker({
|
||||
acceptedFileTypes: "image/*"
|
||||
});
|
||||
if (!file) return;
|
||||
setProfilePicture(file);
|
||||
setScale(1);
|
||||
}}
|
||||
>
|
||||
{typeof profilePicture === "string" ? "Clear" : "Reset"}
|
||||
{profilePicture
|
||||
? "Change profile picture"
|
||||
: "Select profile picture"}
|
||||
</Button>
|
||||
{profilePicture ? (
|
||||
<Button
|
||||
sx={{ flex: 1 }}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
if (typeof profilePicture === "string") {
|
||||
setClearProfilePicture(true);
|
||||
setProfilePicture(undefined);
|
||||
} else setProfilePicture(profile?.profilePicture);
|
||||
setScale(1);
|
||||
}}
|
||||
>
|
||||
{typeof profilePicture === "string" ? "Clear" : "Reset"}
|
||||
</Button>
|
||||
) : null}
|
||||
</Flex>
|
||||
{profilePicture ? (
|
||||
<Slider
|
||||
max={5}
|
||||
min={1}
|
||||
step={0.1}
|
||||
value={scale}
|
||||
sx={{ color: "accent" }}
|
||||
onChange={(e) => setScale(e.target.valueAsNumber)}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
{profilePicture ? (
|
||||
<Slider
|
||||
max={5}
|
||||
min={1}
|
||||
step={0.1}
|
||||
value={scale}
|
||||
sx={{ color: "accent" }}
|
||||
onChange={(e) => setScale(e.target.valueAsNumber)}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -20,162 +20,162 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { db } from "../common/db";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { useTimer } from "../hooks/use-timer";
|
||||
import Field from "../components/field";
|
||||
import { Loading } from "../components/icons";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type EmailChangeState = {
|
||||
newEmail: string;
|
||||
password: string;
|
||||
};
|
||||
export type EmailChangeDialogProps = {
|
||||
onClose: Perform;
|
||||
};
|
||||
export type EmailChangeDialogProps = BaseDialogProps<boolean>;
|
||||
export const EmailChangeDialog = DialogManager.register(
|
||||
function EmailChangeDialog(props: EmailChangeDialogProps) {
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [emailChangeState, setEmailChangeState] =
|
||||
useState<EmailChangeState>();
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const { elapsed, enabled, setEnabled } = useTimer(`email_change_code`, 60);
|
||||
|
||||
export default function EmailChangeDialog(props: EmailChangeDialogProps) {
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [emailChangeState, setEmailChangeState] = useState<EmailChangeState>();
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const { elapsed, enabled, setEnabled } = useTimer(`email_change_code`, 60);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const codeRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const codeRef = useRef<HTMLInputElement>(null);
|
||||
const sendCode = useCallback(
|
||||
async (emailChangeState: EmailChangeState) => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
await db.user.sendVerificationEmail(emailChangeState.newEmail);
|
||||
|
||||
const sendCode = useCallback(
|
||||
async (emailChangeState: EmailChangeState) => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
await db.user.sendVerificationEmail(emailChangeState.newEmail);
|
||||
setEnabled(false);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
},
|
||||
[setEnabled]
|
||||
);
|
||||
|
||||
setEnabled(false);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
setError(error.message);
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
},
|
||||
[setEnabled]
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Change account email"}
|
||||
description={
|
||||
"Your account email will be changed without affecting your subscription or any other settings."
|
||||
}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
text: "Next",
|
||||
disabled: isLoading,
|
||||
loading: isLoading,
|
||||
form: "changeEmailForm"
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Flex
|
||||
id="changeEmailForm"
|
||||
as="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Change account email"}
|
||||
description={
|
||||
"Your account email will be changed without affecting your subscription or any other settings."
|
||||
}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
text: "Next",
|
||||
disabled: isLoading,
|
||||
loading: isLoading,
|
||||
form: "changeEmailForm"
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
<Flex
|
||||
id="changeEmailForm"
|
||||
as="form"
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(undefined);
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const { password, newEmail, code } = Object.fromEntries(
|
||||
formData.entries() as IterableIterator<[string, string]>
|
||||
);
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement);
|
||||
const { password, newEmail, code } = Object.fromEntries(
|
||||
formData.entries() as IterableIterator<[string, string]>
|
||||
);
|
||||
if (emailChangeState) {
|
||||
if (!code || code.length < 6) {
|
||||
setError("Please enter a valid verification code.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailChangeState) {
|
||||
if (!code || code.length < 6) {
|
||||
setError("Please enter a valid verification code.");
|
||||
await db.user.changeEmail(
|
||||
emailChangeState.newEmail,
|
||||
emailChangeState.password,
|
||||
code
|
||||
);
|
||||
props.onClose(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await db.user.changeEmail(
|
||||
emailChangeState.newEmail,
|
||||
emailChangeState.password,
|
||||
code
|
||||
);
|
||||
props.onClose(true);
|
||||
return;
|
||||
}
|
||||
if (!newEmail.trim() || !password.trim()) return;
|
||||
|
||||
if (!newEmail.trim() || !password.trim()) return;
|
||||
|
||||
if (!password || !(await db.user.verifyPassword(password))) {
|
||||
setError("Password is not correct.");
|
||||
return;
|
||||
}
|
||||
|
||||
await db.user.sendVerificationEmail(newEmail);
|
||||
setEmailChangeState({ newEmail, password });
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
sx={{ flexDirection: "column" }}
|
||||
>
|
||||
{emailChangeState ? (
|
||||
<Field
|
||||
inputRef={codeRef}
|
||||
id="code"
|
||||
name="code"
|
||||
label="6-digit code"
|
||||
helpText={`Enter 6-digit code sent to ${emailChangeState.newEmail}`}
|
||||
type="text"
|
||||
required
|
||||
action={{
|
||||
disabled: isSending || !enabled,
|
||||
component: (
|
||||
<Text variant={"body"}>
|
||||
{isSending ? (
|
||||
<Loading size={18} />
|
||||
) : enabled ? (
|
||||
`Resend code`
|
||||
) : (
|
||||
`Resend in ${elapsed}`
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
onClick: async () => {
|
||||
await sendCode(emailChangeState);
|
||||
if (!password || !(await db.user.verifyPassword(password))) {
|
||||
setError("Password is not correct.");
|
||||
return;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
||||
await db.user.sendVerificationEmail(newEmail);
|
||||
setEmailChangeState({ newEmail, password });
|
||||
} catch (e) {
|
||||
if (e instanceof Error) setError(e.message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
sx={{ flexDirection: "column" }}
|
||||
>
|
||||
{emailChangeState ? (
|
||||
<Field
|
||||
inputRef={emailRef}
|
||||
id="newEmail"
|
||||
name="newEmail"
|
||||
label="New email"
|
||||
type="email"
|
||||
inputRef={codeRef}
|
||||
id="code"
|
||||
name="code"
|
||||
label="6-digit code"
|
||||
helpText={`Enter 6-digit code sent to ${emailChangeState.newEmail}`}
|
||||
type="text"
|
||||
required
|
||||
action={{
|
||||
disabled: isSending || !enabled,
|
||||
component: (
|
||||
<Text variant={"body"}>
|
||||
{isSending ? (
|
||||
<Loading size={18} />
|
||||
) : enabled ? (
|
||||
`Resend code`
|
||||
) : (
|
||||
`Resend in ${elapsed}`
|
||||
)}
|
||||
</Text>
|
||||
),
|
||||
onClick: async () => {
|
||||
await sendCode(emailChangeState);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
inputRef={passwordRef}
|
||||
id="password"
|
||||
name="password"
|
||||
label="Your account password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
You will be logged out from all your other devices.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{error && <Text variant="error">{error}</Text>}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
) : (
|
||||
<>
|
||||
<Field
|
||||
inputRef={emailRef}
|
||||
id="newEmail"
|
||||
name="newEmail"
|
||||
label="New email"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
<Field
|
||||
inputRef={passwordRef}
|
||||
id="password"
|
||||
name="password"
|
||||
label="Your account password"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
<Text variant="subBody" sx={{ mt: 1 }}>
|
||||
You will be logged out from all your other devices.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{error && <Text variant="error">{error}</Text>}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,120 +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 } from "react";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { useStore as useUserStore } from "../stores/user-store";
|
||||
import { db } from "../common/db";
|
||||
import { useState } from "react";
|
||||
import { useSessionState } from "../hooks/use-session-state";
|
||||
import Accordion from "../components/accordion";
|
||||
|
||||
var interval = 0;
|
||||
function EmailVerificationDialog(props) {
|
||||
const user = useUserStore((store) => store.user);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [canSendAgain, setCanSendAgain] = useSessionState("canSendAgain", true);
|
||||
const [resetTimer, setResetTimer] = useSessionState("resetTimer", 60);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canSendAgain) {
|
||||
interval = setInterval(() => {
|
||||
setResetTimer((s) => {
|
||||
--s;
|
||||
if (s <= 0) {
|
||||
setCanSendAgain(true);
|
||||
clearInterval(interval);
|
||||
return 60;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [canSendAgain, setResetTimer, setCanSendAgain]);
|
||||
|
||||
if (!user) {
|
||||
props.onCancel();
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Confirm your email"}
|
||||
description={
|
||||
"Check your spam folder if you haven't received an email yet."
|
||||
}
|
||||
onClose={() => props.onCancel(false)}
|
||||
positiveButton={{
|
||||
text: canSendAgain || isSending ? "Resend" : `Resend (${resetTimer})`,
|
||||
onClick: async () => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
await db.user.sendVerificationEmail();
|
||||
setCanSendAgain(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
},
|
||||
loading: isSending,
|
||||
disabled: isSending || !canSendAgain
|
||||
}}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: () => props.onCancel(true),
|
||||
disabled: isSending
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
as="span"
|
||||
variant="body"
|
||||
sx={{ borderRadius: "default", alignSelf: "stretch" }}
|
||||
>
|
||||
We have sent the email confirmation link at{" "}
|
||||
<Text as="b" sx={{ color: "accent" }}>
|
||||
{user.email}
|
||||
</Text>
|
||||
.
|
||||
</Text>
|
||||
<Accordion
|
||||
title={"What do I do if I am not getting the email?"}
|
||||
sx={{
|
||||
mt: 2,
|
||||
bg: "var(--background-secondary)",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
<Text variant={"body"} px={1} pb={1}>
|
||||
{`If you didn't get an email from us or the confirmation link isn't
|
||||
working,`}{" "}
|
||||
<b>please send us an email from your registered email address</b>{" "}
|
||||
and we will manually confirm your account.
|
||||
</Text>
|
||||
</Accordion>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default EmailVerificationDialog;
|
||||
127
apps/web/src/dialogs/email-verification-dialog.tsx
Normal file
127
apps/web/src/dialogs/email-verification-dialog.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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 } from "react";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { useStore as useUserStore } from "../stores/user-store";
|
||||
import { db } from "../common/db";
|
||||
import { useState } from "react";
|
||||
import { useSessionState } from "../hooks/use-session-state";
|
||||
import Accordion from "../components/accordion";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
let interval = 0;
|
||||
type EmailVerificationDialogProps = BaseDialogProps<boolean>;
|
||||
export const EmailVerificationDialog = DialogManager.register(
|
||||
function EmailVerificationDialog(props: EmailVerificationDialogProps) {
|
||||
const user = useUserStore((store) => store.user);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [canSendAgain, setCanSendAgain] = useSessionState(
|
||||
"canSendAgain",
|
||||
true
|
||||
);
|
||||
const [resetTimer, setResetTimer] = useSessionState("resetTimer", 60);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canSendAgain) {
|
||||
interval = setInterval(() => {
|
||||
setResetTimer((s) => {
|
||||
--s;
|
||||
if (s <= 0) {
|
||||
setCanSendAgain(true);
|
||||
clearInterval(interval);
|
||||
return 60;
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}, 1000) as unknown as number;
|
||||
}
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [canSendAgain, setResetTimer, setCanSendAgain]);
|
||||
|
||||
if (!user) {
|
||||
props.onClose(false);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Confirm your email"}
|
||||
description={
|
||||
"Check your spam folder if you haven't received an email yet."
|
||||
}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
text: canSendAgain || isSending ? "Resend" : `Resend (${resetTimer})`,
|
||||
onClick: async () => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
await db.user.sendVerificationEmail();
|
||||
setCanSendAgain(false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
},
|
||||
loading: isSending,
|
||||
disabled: isSending || !canSendAgain
|
||||
}}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: () => props.onClose(true),
|
||||
disabled: isSending
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
as="span"
|
||||
variant="body"
|
||||
sx={{ borderRadius: "default", alignSelf: "stretch" }}
|
||||
>
|
||||
We have sent the email confirmation link at{" "}
|
||||
<Text as="b" sx={{ color: "accent" }}>
|
||||
{user.email}
|
||||
</Text>
|
||||
.
|
||||
</Text>
|
||||
<Accordion
|
||||
isClosed
|
||||
title={"What do I do if I am not getting the email?"}
|
||||
sx={{
|
||||
mt: 2,
|
||||
bg: "var(--background-secondary)",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
>
|
||||
<Text variant={"body"} px={1} pb={1}>
|
||||
{`If you didn't get an email from us or the confirmation link isn't
|
||||
working,`}{" "}
|
||||
<b>please send us an email from your registered email address</b>{" "}
|
||||
and we will manually confirm your account.
|
||||
</Text>
|
||||
</Accordion>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -25,6 +25,7 @@ import Config from "../utils/config";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { ArrowRight, Checkmark, Icon, Warn } from "../components/icons";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type CallToAction = {
|
||||
title: string;
|
||||
@@ -113,12 +114,13 @@ const features: Record<FeatureKeys, Feature> = {
|
||||
}
|
||||
};
|
||||
|
||||
type FeatureDialogProps = {
|
||||
type FeatureDialogProps = BaseDialogProps<boolean> & {
|
||||
featureName: FeatureKeys;
|
||||
onClose: (result: boolean) => void;
|
||||
};
|
||||
|
||||
function FeatureDialog(props: FeatureDialogProps) {
|
||||
export const FeatureDialog = DialogManager.register(function FeatureDialog(
|
||||
props: FeatureDialogProps
|
||||
) {
|
||||
const { featureName, onClose } = props;
|
||||
const feature = features[featureName];
|
||||
|
||||
@@ -178,8 +180,7 @@ function FeatureDialog(props: FeatureDialogProps) {
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default FeatureDialog;
|
||||
});
|
||||
|
||||
type CodeProps = { text: string; href?: string };
|
||||
export function Code(props: CodeProps) {
|
||||
|
||||
@@ -18,15 +18,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Dialog from "../components/dialog";
|
||||
import { ScrollContainer } from "@notesnook/ui";
|
||||
import { Flex, Image, Label, Text } from "@theme-ui/components";
|
||||
import { formatBytes } from "@notesnook/common";
|
||||
import { compressImage } from "../utils/image-compressor";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
export type ImagePickerDialogProps = {
|
||||
onClose: Perform<false | File[]>;
|
||||
export type ImagePickerDialogProps = BaseDialogProps<false | File[]> & {
|
||||
images: File[];
|
||||
};
|
||||
class FileWithURI extends File {
|
||||
@@ -41,106 +40,110 @@ class FileWithURI extends File {
|
||||
}
|
||||
}
|
||||
|
||||
export default function ImagePickerDialog(props: ImagePickerDialogProps) {
|
||||
const [images, setImages] = useState<FileWithURI[]>([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [compress, setCompress] = useState(true);
|
||||
const selectedImage = images[selectedIndex];
|
||||
export const ImagePickerDialog = DialogManager.register(
|
||||
function ImagePickerDialog(props: ImagePickerDialogProps) {
|
||||
const [images, setImages] = useState<FileWithURI[]>([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [compress, setCompress] = useState(true);
|
||||
const selectedImage = images[selectedIndex];
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const images: FileWithURI[] = [];
|
||||
for (const image of props.images) {
|
||||
const compressed = compress
|
||||
? await compressImage(image, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
width: (naturalWidth) => naturalWidth,
|
||||
height: (_, naturalHeight) => naturalHeight,
|
||||
resize: "contain",
|
||||
quality: 0.7
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
const images: FileWithURI[] = [];
|
||||
for (const image of props.images) {
|
||||
const compressed = compress
|
||||
? await compressImage(image, {
|
||||
maxWidth: (naturalWidth) => Math.min(1920, naturalWidth * 0.7),
|
||||
width: (naturalWidth) => naturalWidth,
|
||||
height: (_, naturalHeight) => naturalHeight,
|
||||
resize: "contain",
|
||||
quality: 0.7
|
||||
})
|
||||
: image;
|
||||
images.push(
|
||||
new FileWithURI([compressed], image.name, {
|
||||
lastModified: image.lastModified,
|
||||
type: image.type
|
||||
})
|
||||
: image;
|
||||
images.push(
|
||||
new FileWithURI([compressed], image.name, {
|
||||
lastModified: image.lastModified,
|
||||
type: image.type
|
||||
})
|
||||
);
|
||||
}
|
||||
setImages(images);
|
||||
})();
|
||||
}, [props.images, compress]);
|
||||
);
|
||||
}
|
||||
setImages(images);
|
||||
})();
|
||||
}, [props.images, compress]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
images.forEach((i) => URL.revokeObjectURL(i.uri));
|
||||
};
|
||||
}, [images]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
images.forEach((i) => URL.revokeObjectURL(i.uri));
|
||||
};
|
||||
}, [images]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
text: "Insert",
|
||||
onClick: () => props.onClose(images)
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
{selectedImage && (
|
||||
<Flex sx={{ flexDirection: "column", alignItems: "center", mt: 4 }}>
|
||||
<Image
|
||||
src={selectedImage.uri}
|
||||
sx={{
|
||||
maxHeight: 250,
|
||||
objectFit: "contain",
|
||||
alignSelf: "center",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
/>
|
||||
<Text variant="subBody" sx={{ my: 2 }}>
|
||||
{selectedImage.name} ({formatBytes(selectedImage.size)})
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{images.length > 1 ? (
|
||||
<ScrollContainer style={{ display: "flex" }}>
|
||||
{images.map((image, index) => (
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
text: "Insert",
|
||||
onClick: () => props.onClose(images)
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
{selectedImage && (
|
||||
<Flex sx={{ flexDirection: "column", alignItems: "center", mt: 4 }}>
|
||||
<Image
|
||||
key={image.name + image.size}
|
||||
src={image.uri}
|
||||
src={selectedImage.uri}
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
height: "55px",
|
||||
width: "55px",
|
||||
maxHeight: 250,
|
||||
objectFit: "contain",
|
||||
border:
|
||||
selectedIndex === index
|
||||
? "2px solid var(--accent)"
|
||||
: "2px solid transparent",
|
||||
alignSelf: "center",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
) : null}
|
||||
<Text variant="subBody" sx={{ my: 2 }}>
|
||||
{selectedImage.name} ({formatBytes(selectedImage.size)})
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{images.length > 1 ? (
|
||||
<ScrollContainer style={{ display: "flex" }}>
|
||||
{images.map((image, index) => (
|
||||
<Image
|
||||
key={image.name + image.size}
|
||||
src={image.uri}
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
height: "55px",
|
||||
width: "55px",
|
||||
objectFit: "contain",
|
||||
border:
|
||||
selectedIndex === index
|
||||
? "2px solid var(--accent)"
|
||||
: "2px solid transparent",
|
||||
borderRadius: "default"
|
||||
}}
|
||||
onClick={() => setSelectedIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
) : null}
|
||||
|
||||
<Label variant="text.body" sx={{ mt: 2 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{
|
||||
accentColor: "var(--accent)",
|
||||
marginRight: 1,
|
||||
width: 14,
|
||||
height: 14
|
||||
}}
|
||||
defaultChecked={compress}
|
||||
checked={compress}
|
||||
onChange={() => setCompress((s) => !s)}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>Enable compression (recommended)</span>
|
||||
</Label>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
<Label variant="text.body" sx={{ mt: 2 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{
|
||||
accentColor: "var(--accent)",
|
||||
marginRight: 1,
|
||||
width: 14,
|
||||
height: 14
|
||||
}}
|
||||
defaultChecked={compress}
|
||||
checked={compress}
|
||||
onChange={() => setCompress((s) => !s)}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>
|
||||
Enable compression (recommended)
|
||||
</span>
|
||||
</Label>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import SettingsDialog from "./settings";
|
||||
|
||||
const AddNotebookDialog = React.lazy(() => import("./add-notebook-dialog"));
|
||||
const BuyDialog = React.lazy(() => import("./buy-dialog"));
|
||||
const Confirm = React.lazy(() => import("./confirm"));
|
||||
const EmailVerificationDialog = React.lazy(
|
||||
() => import("./email-verification-dialog")
|
||||
);
|
||||
const LoadingDialog = React.lazy(() => import("./loading-dialog"));
|
||||
const ProgressDialog = React.lazy(() => import("./progress-dialog"));
|
||||
const MoveDialog = React.lazy(() => import("./move-note-dialog"));
|
||||
const PasswordDialog = React.lazy(() => import("./password-dialog"));
|
||||
const RecoveryKeyDialog = React.lazy(() => import("./recovery-key-dialog"));
|
||||
const ItemDialog = React.lazy(() => import("./item-dialog"));
|
||||
const FeatureDialog = React.lazy(() => import("./feature-dialog"));
|
||||
const ReminderDialog = React.lazy(() => import("./reminder-dialog"));
|
||||
const AddReminderDialog = React.lazy(() => import("./add-reminder-dialog"));
|
||||
const ReminderPreviewDialog = React.lazy(
|
||||
() => import("./reminder-preview-dialog")
|
||||
);
|
||||
const AnnouncementDialog = React.lazy(() => import("./announcement-dialog"));
|
||||
const IssueDialog = React.lazy(() => import("./issue-dialog"));
|
||||
const MultifactorDialog = React.lazy(() => import("./mfa/multi-factor-dialog"));
|
||||
const RecoveryCodesDialog = React.lazy(
|
||||
() => import("./mfa/recovery-code-dialog")
|
||||
);
|
||||
// import { MultifactorDialog, RecoveryCodesDialog } from "./multi-factor-dialog";
|
||||
const OnboardingDialog = React.lazy(() => import("./onboarding-dialog"));
|
||||
const AttachmentsDialog = React.lazy(() => import("./attachments-dialog"));
|
||||
const Prompt = React.lazy(() => import("./prompt"));
|
||||
const MigrationDialog = React.lazy(() => import("./migration-dialog"));
|
||||
const EmailChangeDialog = React.lazy(() => import("./email-change-dialog"));
|
||||
const AddTagsDialog = React.lazy(() => import("./add-tags-dialog"));
|
||||
const ThemeDetailsDialog = React.lazy(() => import("./theme-details-dialog"));
|
||||
const BackupPasswordDialog = React.lazy(
|
||||
() => import("./backup-password-dialog")
|
||||
);
|
||||
const CreateColorDialog = React.lazy(() => import("./create-color-dialog"));
|
||||
const EditProfilePictureDialog = React.lazy(
|
||||
() => import("./edit-profile-picture-dialog")
|
||||
);
|
||||
const ImagePickerDialog = React.lazy(() => import("./image-picker-dialog"));
|
||||
const NoteLinkingDialog = React.lazy(() => import("./note-linking-dialog"));
|
||||
|
||||
export const Dialogs = {
|
||||
AddNotebookDialog,
|
||||
BuyDialog,
|
||||
Confirm,
|
||||
Prompt,
|
||||
EmailVerificationDialog,
|
||||
LoadingDialog,
|
||||
MoveDialog,
|
||||
PasswordDialog,
|
||||
RecoveryKeyDialog,
|
||||
ItemDialog,
|
||||
FeatureDialog,
|
||||
ProgressDialog,
|
||||
ReminderDialog,
|
||||
AnnouncementDialog,
|
||||
IssueDialog,
|
||||
MultifactorDialog,
|
||||
RecoveryCodesDialog,
|
||||
OnboardingDialog,
|
||||
AttachmentsDialog,
|
||||
MigrationDialog,
|
||||
AddReminderDialog,
|
||||
ReminderPreviewDialog,
|
||||
EmailChangeDialog,
|
||||
AddTagsDialog,
|
||||
SettingsDialog,
|
||||
ThemeDetailsDialog,
|
||||
BackupPasswordDialog,
|
||||
CreateColorDialog,
|
||||
EditProfilePictureDialog,
|
||||
ImagePickerDialog,
|
||||
NoteLinkingDialog
|
||||
};
|
||||
@@ -23,13 +23,14 @@ import Field from "../components/field";
|
||||
import Dialog from "../components/dialog";
|
||||
import platform from "platform";
|
||||
import { useState } from "react";
|
||||
import { confirm, Perform } from "../common/dialog-controller";
|
||||
import { isUserPremium } from "../hooks/use-is-user-premium";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { store as userstore } from "../stores/user-store";
|
||||
|
||||
import { ErrorText } from "../components/error-text";
|
||||
import { Debug } from "@notesnook/core/dist/api/debug";
|
||||
import { ConfirmDialog } from "./confirm";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
const PLACEHOLDERS = {
|
||||
title: "Briefly describe what happened",
|
||||
@@ -43,10 +44,10 @@ For example things like:
|
||||
This is all optional, of course.`
|
||||
};
|
||||
|
||||
type IssueDialogProps = {
|
||||
onClose: Perform;
|
||||
};
|
||||
function IssueDialog(props: IssueDialogProps) {
|
||||
type IssueDialogProps = BaseDialogProps<boolean>;
|
||||
export const IssueDialog = DialogManager.register(function IssueDialog(
|
||||
props: IssueDialogProps
|
||||
) {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
@@ -143,12 +144,10 @@ function IssueDialog(props: IssueDialogProps) {
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default IssueDialog;
|
||||
});
|
||||
|
||||
function showIssueReportedDialog({ url }: { url: string }) {
|
||||
return confirm({
|
||||
return ConfirmDialog.show({
|
||||
title: "Thank you for reporting!",
|
||||
positiveButtonText: "Copy link",
|
||||
message: `You can track your bug report at [${url}](${url}).
|
||||
|
||||
@@ -20,18 +20,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { Box } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import Field from "../components/field";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { useState } from "react";
|
||||
import { ErrorText } from "../components/error-text";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { db } from "../common/db";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { useStore as useTagStore } from "../stores/tag-store";
|
||||
import { useStore as useNoteStore } from "../stores/note-store";
|
||||
import { useStore as useAppStore } from "../stores/app-store";
|
||||
import { Color, Tag } from "@notesnook/core";
|
||||
|
||||
type ItemDialogProps = {
|
||||
type ItemDialogProps = BaseDialogProps<false | string> & {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
onClose: Perform;
|
||||
onAction: (title: string) => Promise<void>;
|
||||
defaultValue?: string;
|
||||
};
|
||||
function ItemDialog(props: ItemDialogProps) {
|
||||
export const ItemDialog = DialogManager.register(function ItemDialog(
|
||||
props: ItemDialogProps
|
||||
) {
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
return (
|
||||
@@ -58,7 +64,7 @@ function ItemDialog(props: ItemDialogProps) {
|
||||
const title = formData.get("title");
|
||||
if (!title) return;
|
||||
try {
|
||||
await props.onAction(title as string);
|
||||
await props.onClose(title as string);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e);
|
||||
@@ -79,6 +85,47 @@ function ItemDialog(props: ItemDialogProps) {
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default ItemDialog;
|
||||
export const CreateTagDialog = {
|
||||
show: () =>
|
||||
ItemDialog.show({
|
||||
title: "Create tag",
|
||||
subtitle: "You can create as many tags as you want."
|
||||
}).then(async (title) => {
|
||||
if (!title) return;
|
||||
await db.tags.add({ title });
|
||||
showToast("success", "Tag created!");
|
||||
useTagStore.getState().refresh();
|
||||
})
|
||||
};
|
||||
|
||||
export const EditTagDialog = {
|
||||
show: (tag: Tag) =>
|
||||
ItemDialog.show({
|
||||
title: "Edit tag",
|
||||
subtitle: `You are editing #${tag.title}.`,
|
||||
defaultValue: tag.title
|
||||
}).then(async (title) => {
|
||||
if (!title) return;
|
||||
await db.tags.add({ id: tag.id, title });
|
||||
showToast("success", "Tag edited!");
|
||||
await useTagStore.getState().refresh();
|
||||
await useNoteStore.getState().refresh();
|
||||
await useAppStore.getState().refreshNavItems();
|
||||
})
|
||||
};
|
||||
|
||||
export const RenameColorDialog = {
|
||||
show: (color: Color) =>
|
||||
ItemDialog.show({
|
||||
title: "Rename color",
|
||||
subtitle: `You are renaming color ${color.title}.`,
|
||||
defaultValue: color.title
|
||||
}).then(async (title) => {
|
||||
if (!title) return;
|
||||
await db.colors.add({ id: color.id, title });
|
||||
showToast("success", "Color renamed!");
|
||||
useAppStore.getState().refreshNavItems();
|
||||
})
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Perform } from "../../common/dialog-controller";
|
||||
import Dialog from "../../components/dialog";
|
||||
import {
|
||||
AuthenticatorOnNext,
|
||||
@@ -31,65 +30,67 @@ import {
|
||||
import { Authenticator, OnNextFunction } from "./types";
|
||||
import { ErrorText } from "../../components/error-text";
|
||||
import { AuthenticatorType } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
|
||||
|
||||
type MultifactorDialogProps = {
|
||||
onClose: Perform;
|
||||
type MultifactorDialogProps = BaseDialogProps<boolean> & {
|
||||
primaryMethod?: AuthenticatorType;
|
||||
};
|
||||
|
||||
export default function MultifactorDialog(props: MultifactorDialogProps) {
|
||||
const { onClose, primaryMethod } = props;
|
||||
const [step, setStep] = useState<
|
||||
| Step<AuthenticatorOnNext>
|
||||
| Step<AuthenticatorTypeOnNext>
|
||||
| FallbackStep<OnNextFunction>
|
||||
>(primaryMethod ? fallbackSteps.choose(primaryMethod) : steps.choose());
|
||||
const [error, setError] = useState<string>();
|
||||
export const MultifactorDialog = DialogManager.register(
|
||||
function MultifactorDialog(props: MultifactorDialogProps) {
|
||||
const { onClose, primaryMethod } = props;
|
||||
const [step, setStep] = useState<
|
||||
| Step<AuthenticatorOnNext>
|
||||
| Step<AuthenticatorTypeOnNext>
|
||||
| FallbackStep<OnNextFunction>
|
||||
>(primaryMethod ? fallbackSteps.choose(primaryMethod) : steps.choose());
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
if (!step) return null;
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={step.title}
|
||||
description={step.description}
|
||||
width={500}
|
||||
positiveButton={
|
||||
step.next
|
||||
? {
|
||||
text: "Continue",
|
||||
form: "2faForm"
|
||||
}
|
||||
: null
|
||||
}
|
||||
negativeButton={
|
||||
step.cancellable
|
||||
? {
|
||||
text: "Cancel",
|
||||
onClick: () => props.onClose(false)
|
||||
}
|
||||
: null
|
||||
}
|
||||
>
|
||||
{step.component && (
|
||||
<step.component
|
||||
onNext={(arg: AuthenticatorType | Authenticator) => {
|
||||
if (!step.next) return onClose(true);
|
||||
if (!step) return null;
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={step.title}
|
||||
description={step.description}
|
||||
width={500}
|
||||
positiveButton={
|
||||
step.next
|
||||
? {
|
||||
text: "Continue",
|
||||
form: "2faForm"
|
||||
}
|
||||
: null
|
||||
}
|
||||
negativeButton={
|
||||
step.cancellable
|
||||
? {
|
||||
text: "Cancel",
|
||||
onClick: () => props.onClose(false)
|
||||
}
|
||||
: null
|
||||
}
|
||||
>
|
||||
{step.component && (
|
||||
<step.component
|
||||
onNext={(arg: AuthenticatorType | Authenticator) => {
|
||||
if (!step.next) return onClose(true);
|
||||
|
||||
const nextStep =
|
||||
step.next !== "recoveryCodes" && primaryMethod
|
||||
? fallbackSteps[step.next](
|
||||
arg as AuthenticatorType & Authenticator,
|
||||
primaryMethod
|
||||
)
|
||||
: steps[step.next](arg as AuthenticatorType & Authenticator);
|
||||
const nextStep =
|
||||
step.next !== "recoveryCodes" && primaryMethod
|
||||
? fallbackSteps[step.next](
|
||||
arg as AuthenticatorType & Authenticator,
|
||||
primaryMethod
|
||||
)
|
||||
: steps[step.next](arg as AuthenticatorType & Authenticator);
|
||||
|
||||
setStep(nextStep);
|
||||
}}
|
||||
onError={setError}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
<ErrorText error={error} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
setStep(nextStep);
|
||||
}}
|
||||
onError={setError}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
<ErrorText error={error} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,37 +18,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Perform } from "../../common/dialog-controller";
|
||||
import Dialog from "../../components/dialog";
|
||||
import { steps } from "./steps";
|
||||
import { ErrorText } from "../../components/error-text";
|
||||
import { AuthenticatorType } from "@notesnook/core";
|
||||
import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
|
||||
|
||||
type RecoveryCodesDialogProps = {
|
||||
onClose: Perform;
|
||||
type RecoveryCodesDialogProps = BaseDialogProps<boolean> & {
|
||||
primaryMethod: AuthenticatorType;
|
||||
};
|
||||
|
||||
export default function RecoveryCodesDialog(props: RecoveryCodesDialogProps) {
|
||||
const { onClose, primaryMethod } = props;
|
||||
const [error, setError] = useState<string>();
|
||||
const step = steps.recoveryCodes(primaryMethod);
|
||||
export const RecoveryCodesDialog = DialogManager.register(
|
||||
function RecoveryCodesDialog(props: RecoveryCodesDialogProps) {
|
||||
const { onClose, primaryMethod } = props;
|
||||
const [error, setError] = useState<string>();
|
||||
const step = steps.recoveryCodes(primaryMethod);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={step.title}
|
||||
description={step.description}
|
||||
width={500}
|
||||
positiveButton={{
|
||||
text: "Okay",
|
||||
onClick: () => onClose(true)
|
||||
}}
|
||||
>
|
||||
{step.component && (
|
||||
<step.component onNext={() => {}} onError={setError} />
|
||||
)}
|
||||
<ErrorText error={error} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={step.title}
|
||||
description={step.description}
|
||||
width={500}
|
||||
positiveButton={{
|
||||
text: "Okay",
|
||||
onClick: () => onClose(true)
|
||||
}}
|
||||
>
|
||||
{step.component && (
|
||||
<step.component onNext={() => {}} onError={setError} />
|
||||
)}
|
||||
<ErrorText error={error} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -55,9 +55,9 @@ import {
|
||||
StepComponentProps,
|
||||
OnNextFunction
|
||||
} from "./types";
|
||||
import { showMultifactorDialog } from "../../common/dialog-controller";
|
||||
import { ErrorText } from "../../components/error-text";
|
||||
import { AuthenticatorType } from "@notesnook/core";
|
||||
import { MultifactorDialog } from "./multi-factor-dialog";
|
||||
|
||||
const QRCode = React.lazy(() => import("../../re-exports/react-qrcode-logo"));
|
||||
|
||||
@@ -734,7 +734,9 @@ function TwoFactorEnabled(props: TwoFactorEnabledProps) {
|
||||
onClick={() => {
|
||||
props.onClose && props.onClose(true);
|
||||
setTimeout(async () => {
|
||||
await showMultifactorDialog(props.authenticatorType);
|
||||
await MultifactorDialog.show({
|
||||
primaryMethod: props.authenticatorType
|
||||
});
|
||||
}, 100);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { AuthenticatorType } from "@notesnook/core";
|
||||
import { Perform } from "../../common/dialog-controller";
|
||||
import { Icon } from "../../components/icons";
|
||||
|
||||
export type Authenticator = {
|
||||
@@ -31,7 +30,7 @@ export type Authenticator = {
|
||||
|
||||
export type StepComponentProps<T extends OnNextFunction> = {
|
||||
onNext: T;
|
||||
onClose?: Perform;
|
||||
onClose?: (result: boolean) => void;
|
||||
onError?: (error: string) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,11 +22,10 @@ import { Text } from "@theme-ui/components";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { createBackup } from "../common";
|
||||
import { db } from "../common/db";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { TaskManager } from "../common/task-manager";
|
||||
|
||||
import Dialog from "../components/dialog";
|
||||
import { ErrorText } from "../components/error-text";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type MigrationProgressEvent = {
|
||||
collection: string;
|
||||
@@ -34,11 +33,10 @@ type MigrationProgressEvent = {
|
||||
current: number;
|
||||
};
|
||||
|
||||
export type MigrationDialogProps = {
|
||||
onClose: Perform;
|
||||
};
|
||||
|
||||
export default function MigrationDialog(props: MigrationDialogProps) {
|
||||
export type MigrationDialogProps = BaseDialogProps<boolean>;
|
||||
export const MigrationDialog = DialogManager.register(function MigrationDialog(
|
||||
props: MigrationDialogProps
|
||||
) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
@@ -133,4 +131,4 @@ export default function MigrationDialog(props: MigrationDialogProps) {
|
||||
</Text>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ import Dialog from "../components/dialog";
|
||||
import { useStore } from "../stores/notebook-store";
|
||||
import { store as noteStore } from "../stores/note-store";
|
||||
import { store as notebookStore } from "../stores/notebook-store";
|
||||
import { Perform, showAddNotebookDialog } from "../common/dialog-controller";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { isMac } from "../utils/platform";
|
||||
import { create } from "zustand";
|
||||
@@ -48,8 +47,10 @@ import {
|
||||
import { FlexScrollContainer } from "../components/scroll-container";
|
||||
import { pluralize } from "@notesnook/common";
|
||||
import Field from "../components/field";
|
||||
import { AddNotebookDialog } from "./add-notebook-dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
|
||||
type MoveNoteDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
||||
type NotebookReference = {
|
||||
id: string;
|
||||
new: boolean;
|
||||
@@ -69,7 +70,10 @@ export const useSelectionStore = create<ISelectionStore>((set) => ({
|
||||
setIsMultiselect: (isMultiselect) => set({ isMultiselect })
|
||||
}));
|
||||
|
||||
function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
|
||||
onClose,
|
||||
noteIds
|
||||
}: MoveNoteDialogProps) {
|
||||
const setSelected = useSelectionStore((store) => store.setSelected);
|
||||
const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
|
||||
const isMultiselect = useSelectionStore((store) => store.isMultiselect);
|
||||
@@ -284,6 +288,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
isExpanded={props.context.isExpanded || false}
|
||||
toggle={props.context.toggleExpandedState}
|
||||
onCreateItem={() => {
|
||||
console.log([props.item.index]);
|
||||
reloadItem.current?.([props.item.index]);
|
||||
treeRef.current?.expandItem(
|
||||
props.item.index,
|
||||
@@ -318,10 +323,12 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
variant="secondary"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={() =>
|
||||
showAddNotebookDialog().then(() =>
|
||||
db.notebooks.roots
|
||||
.ids(db.settings.getGroupOptions("notebooks"))
|
||||
.then((ids) => setNotebooks(ids))
|
||||
AddNotebookDialog.show({}).then((res) =>
|
||||
res
|
||||
? db.notebooks.roots
|
||||
.ids(db.settings.getGroupOptions("notebooks"))
|
||||
.then((ids) => setNotebooks(ids))
|
||||
: null
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -331,7 +338,8 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function calculateIndentation(
|
||||
expandable: boolean,
|
||||
depth: number,
|
||||
@@ -443,7 +451,7 @@ function NotebookItem(props: {
|
||||
title="New notebook"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await showAddNotebookDialog(notebook.id);
|
||||
await AddNotebookDialog.show({ parentId: notebook.id });
|
||||
onCreateItem();
|
||||
}}
|
||||
/>
|
||||
@@ -462,8 +470,6 @@ function TopicSelectionIndicator({ notebook }: { notebook: Notebook }) {
|
||||
return <Circle size={8} color="accent" sx={{ mr: 1 }} />;
|
||||
}
|
||||
|
||||
export default MoveDialog;
|
||||
|
||||
function SelectedCheck({
|
||||
item,
|
||||
size = 20,
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Field from "../components/field";
|
||||
import Dialog from "../components/dialog";
|
||||
import { useState } from "react";
|
||||
@@ -35,211 +34,212 @@ import { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
import { NoteResolvedData, ResolvedItem } from "@notesnook/common";
|
||||
import { Lock } from "../components/icons";
|
||||
import { ellipsize } from "@notesnook/core/dist/utils/content-block";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
export type NoteLinkingDialogProps = {
|
||||
export type NoteLinkingDialogProps = BaseDialogProps<LinkAttributes | false> & {
|
||||
attributes?: LinkAttributes;
|
||||
onClose: Perform;
|
||||
onDone: Perform<LinkAttributes>;
|
||||
};
|
||||
const HEADING_TYPES = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
||||
|
||||
export default function NoteLinkingDialog(props: NoteLinkingDialogProps) {
|
||||
const { attributes } = props;
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<NoteType>>();
|
||||
const [selectedNote, setSelectedNote] = useState<NoteType>();
|
||||
const [isNoteLocked, setIsNoteLocked] = useState(false);
|
||||
const [blocks, setBlocks] = useState<ContentBlock[]>([]);
|
||||
const [filteredBlocks, setFilteredBlocks] = useState<
|
||||
ContentBlock[] | undefined
|
||||
>();
|
||||
export const NoteLinkingDialog = DialogManager.register(
|
||||
function NoteLinkingDialog(props: NoteLinkingDialogProps) {
|
||||
const { attributes } = props;
|
||||
const [notes, setNotes] = useState<VirtualizedGrouping<NoteType>>();
|
||||
const [selectedNote, setSelectedNote] = useState<NoteType>();
|
||||
const [isNoteLocked, setIsNoteLocked] = useState(false);
|
||||
const [blocks, setBlocks] = useState<ContentBlock[]>([]);
|
||||
const [filteredBlocks, setFilteredBlocks] = useState<
|
||||
ContentBlock[] | undefined
|
||||
>();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={attributes ? "Edit internal link" : "Link to note"}
|
||||
width={500}
|
||||
onClose={() => props.onClose(false)}
|
||||
onOpen={async () => {
|
||||
setNotes(
|
||||
await db.notes.all.sorted(db.settings.getGroupOptions("home"))
|
||||
);
|
||||
}}
|
||||
positiveButton={{
|
||||
text: "Insert link",
|
||||
disabled: !selectedNote,
|
||||
onClick: () =>
|
||||
selectedNote
|
||||
? props.onDone({
|
||||
title: selectedNote.title,
|
||||
href: createInternalLink("note", selectedNote.id)
|
||||
})
|
||||
: null
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
noScroll
|
||||
>
|
||||
<Flex variant="columnFill" sx={{ mx: 3, overflow: "hidden" }}>
|
||||
{selectedNote ? (
|
||||
<>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder="Type # to only search headings"
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) => {
|
||||
const query = e.target.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
setFilteredBlocks(undefined);
|
||||
} else {
|
||||
setFilteredBlocks(
|
||||
query.startsWith("#")
|
||||
? blocks.filter(
|
||||
(v) =>
|
||||
HEADING_TYPES.includes(v.type) &&
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={attributes ? "Edit internal link" : "Link to note"}
|
||||
width={500}
|
||||
onClose={() => props.onClose(false)}
|
||||
onOpen={async () => {
|
||||
setNotes(
|
||||
await db.notes.all.sorted(db.settings.getGroupOptions("home"))
|
||||
);
|
||||
}}
|
||||
positiveButton={{
|
||||
text: "Insert link",
|
||||
disabled: !selectedNote,
|
||||
onClick: () =>
|
||||
selectedNote
|
||||
? props.onClose({
|
||||
title: selectedNote.title,
|
||||
href: createInternalLink("note", selectedNote.id)
|
||||
})
|
||||
: null
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
noScroll
|
||||
>
|
||||
<Flex variant="columnFill" sx={{ mx: 3, overflow: "hidden" }}>
|
||||
{selectedNote ? (
|
||||
<>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder="Type # to only search headings"
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) => {
|
||||
const query = e.target.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
setFilteredBlocks(undefined);
|
||||
} else {
|
||||
setFilteredBlocks(
|
||||
query.startsWith("#")
|
||||
? blocks.filter(
|
||||
(v) =>
|
||||
HEADING_TYPES.includes(v.type) &&
|
||||
v.content.toLowerCase().includes(query)
|
||||
)
|
||||
: blocks.filter((v) =>
|
||||
v.content.toLowerCase().includes(query)
|
||||
)
|
||||
: blocks.filter((v) =>
|
||||
v.content.toLowerCase().includes(query)
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="accentSecondary"
|
||||
sx={{ mt: 1, textAlign: "left" }}
|
||||
onClick={() => {
|
||||
setSelectedNote(undefined);
|
||||
setIsNoteLocked(false);
|
||||
setFilteredBlocks(undefined);
|
||||
setBlocks([]);
|
||||
}}
|
||||
>
|
||||
Selected note: {selectedNote.title} (click to deselect)
|
||||
</Button>
|
||||
{isNoteLocked ? (
|
||||
<Text variant="body" sx={{ mt: 1 }}>
|
||||
Linking to a specific block is not available for locked notes.
|
||||
</Text>
|
||||
) : blocks.length <= 0 ? (
|
||||
<Text variant="body" sx={{ mt: 1 }}>
|
||||
There are no blocks in this note.
|
||||
</Text>
|
||||
) : null}
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={filteredBlocks || blocks}
|
||||
estimatedSize={34}
|
||||
mode="dynamic"
|
||||
itemGap={5}
|
||||
getItemKey={(i) => blocks[i].id}
|
||||
mt={1}
|
||||
renderItem={({ item }) => (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{
|
||||
p: 1,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid var(--border)"
|
||||
}}
|
||||
onClick={() => {
|
||||
props.onDone({
|
||||
title: selectedNote.title,
|
||||
href: createInternalLink("note", selectedNote.id, {
|
||||
blockId: item.id
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontFamily: "monospace", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{ellipsize(item.content, 200, "end").trim() ||
|
||||
"(empty block)"}
|
||||
</Text>
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{
|
||||
bg: "background-secondary",
|
||||
flexShrink: 0,
|
||||
p: "small",
|
||||
px: 1,
|
||||
borderRadius: "default",
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
>
|
||||
{item.type.toUpperCase()}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder="Search for a note to link to..."
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) => {
|
||||
const query = e.target.value.trim();
|
||||
setNotes(
|
||||
query
|
||||
? await db.lookup.notes(e.target.value).sorted()
|
||||
: await db.notes.all.sorted(
|
||||
db.settings.getGroupOptions("home")
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{notes && (
|
||||
<Button
|
||||
variant="accentSecondary"
|
||||
sx={{ mt: 1, textAlign: "left" }}
|
||||
onClick={() => {
|
||||
setSelectedNote(undefined);
|
||||
setIsNoteLocked(false);
|
||||
setFilteredBlocks(undefined);
|
||||
setBlocks([]);
|
||||
}}
|
||||
>
|
||||
Selected note: {selectedNote.title} (click to deselect)
|
||||
</Button>
|
||||
{isNoteLocked ? (
|
||||
<Text variant="body" sx={{ mt: 1 }}>
|
||||
Linking to a specific block is not available for locked notes.
|
||||
</Text>
|
||||
) : blocks.length <= 0 ? (
|
||||
<Text variant="body" sx={{ mt: 1 }}>
|
||||
There are no blocks in this note.
|
||||
</Text>
|
||||
) : null}
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={notes.placeholders}
|
||||
estimatedSize={28}
|
||||
items={filteredBlocks || blocks}
|
||||
estimatedSize={34}
|
||||
mode="dynamic"
|
||||
itemGap={5}
|
||||
getItemKey={notes.key}
|
||||
getItemKey={(i) => blocks[i].id}
|
||||
mt={1}
|
||||
renderItem={({ index }) => (
|
||||
<ResolvedItem items={notes} index={index} type="note">
|
||||
{({ item: note, data }) => (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{
|
||||
p: 1,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
borderBottom: "1px solid var(--border)"
|
||||
}}
|
||||
onClick={async () => {
|
||||
setSelectedNote(note);
|
||||
setIsNoteLocked(
|
||||
!!(data as NoteResolvedData).locked
|
||||
);
|
||||
setBlocks(await db.notes.contentBlocks(note.id));
|
||||
}}
|
||||
>
|
||||
<Text variant="body">{note.title}</Text>
|
||||
{(data as NoteResolvedData).locked ? (
|
||||
<Lock size={14} />
|
||||
) : null}
|
||||
</Button>
|
||||
)}
|
||||
</ResolvedItem>
|
||||
renderItem={({ item }) => (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{
|
||||
p: 1,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
borderBottom: "1px solid var(--border)"
|
||||
}}
|
||||
onClick={() => {
|
||||
props.onClose({
|
||||
title: selectedNote.title,
|
||||
href: createInternalLink("note", selectedNote.id, {
|
||||
blockId: item.id
|
||||
})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{ fontFamily: "monospace", whiteSpace: "pre-wrap" }}
|
||||
>
|
||||
{ellipsize(item.content, 200, "end").trim() ||
|
||||
"(empty block)"}
|
||||
</Text>
|
||||
<Text
|
||||
variant="subBody"
|
||||
sx={{
|
||||
bg: "background-secondary",
|
||||
flexShrink: 0,
|
||||
p: "small",
|
||||
px: 1,
|
||||
borderRadius: "default",
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
>
|
||||
{item.type.toUpperCase()}
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Field
|
||||
autoFocus
|
||||
placeholder="Search for a note to link to..."
|
||||
sx={{ mx: 0 }}
|
||||
onChange={async (e) => {
|
||||
const query = e.target.value.trim();
|
||||
setNotes(
|
||||
query
|
||||
? await db.lookup.notes(e.target.value).sorted()
|
||||
: await db.notes.all.sorted(
|
||||
db.settings.getGroupOptions("home")
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{notes && (
|
||||
<ScrollContainer>
|
||||
<VirtualizedList
|
||||
items={notes.placeholders}
|
||||
estimatedSize={28}
|
||||
itemGap={5}
|
||||
getItemKey={notes.key}
|
||||
mt={1}
|
||||
renderItem={({ index }) => (
|
||||
<ResolvedItem items={notes} index={index} type="note">
|
||||
{({ item: note, data }) => (
|
||||
<Button
|
||||
variant="menuitem"
|
||||
sx={{
|
||||
p: 1,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
borderBottom: "1px solid var(--border)"
|
||||
}}
|
||||
onClick={async () => {
|
||||
setSelectedNote(note);
|
||||
setIsNoteLocked(
|
||||
!!(data as NoteResolvedData).locked
|
||||
);
|
||||
setBlocks(await db.notes.contentBlocks(note.id));
|
||||
}}
|
||||
>
|
||||
<Text variant="body">{note.title}</Text>
|
||||
{(data as NoteResolvedData).locked ? (
|
||||
<Lock size={14} />
|
||||
) : null}
|
||||
</Button>
|
||||
)}
|
||||
</ResolvedItem>
|
||||
)}
|
||||
/>
|
||||
</ScrollContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -39,7 +39,6 @@ import AppleStore from "../assets/apple.png";
|
||||
import { useStore as useThemeStore } from "../stores/theme-store";
|
||||
import { Checkbox, Label } from "@theme-ui/components";
|
||||
import { Features } from "../components/announcements/body";
|
||||
import { showBuyDialog } from "../common/dialog-controller";
|
||||
import { TaskManager } from "../common/task-manager";
|
||||
import { db } from "../common/db";
|
||||
import { usePersistentState } from "../hooks/use-persistent-state";
|
||||
@@ -47,8 +46,20 @@ import { useCallback, useState } from "react";
|
||||
import Config from "../utils/config";
|
||||
import { isMacStoreApp } from "../utils/platform";
|
||||
import { ErrorText } from "../components/error-text";
|
||||
import { BuyDialog } from "./buy-dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
const newUserSteps = [
|
||||
type Step = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
buttonText?: string;
|
||||
image?: JSX.Element;
|
||||
component?:
|
||||
| (() => JSX.Element)
|
||||
| ((props: { onNext: () => void }) => JSX.Element)
|
||||
| ((props: { onClose: () => void }) => JSX.Element);
|
||||
};
|
||||
const newUserSteps: Step[] = [
|
||||
{
|
||||
title: "Safe & encrypted notes",
|
||||
subtitle: "Write with freedom. Never compromise on privacy again.",
|
||||
@@ -86,7 +97,7 @@ const newUserSteps = [
|
||||
}
|
||||
];
|
||||
|
||||
const proUserSteps = [
|
||||
const proUserSteps: Step[] = [
|
||||
{
|
||||
title: "Welcome to Notesnook Pro",
|
||||
subtitle: "Thank you. You are the proof that privacy always comes first.",
|
||||
@@ -112,7 +123,7 @@ const proUserSteps = [
|
||||
}
|
||||
];
|
||||
|
||||
const trialUserSteps = [
|
||||
const trialUserSteps: Step[] = [
|
||||
{
|
||||
title: "Congratulations!",
|
||||
subtitle: "You 14-day free trial has been activated.",
|
||||
@@ -125,93 +136,111 @@ const onboarding = {
|
||||
new: newUserSteps,
|
||||
pro: proUserSteps,
|
||||
trial: trialUserSteps
|
||||
};
|
||||
} as const;
|
||||
|
||||
export function interruptedOnboarding() {
|
||||
for (let key in onboarding) {
|
||||
export function interruptedOnboarding(): keyof typeof onboarding | undefined {
|
||||
for (const key in onboarding) {
|
||||
const index = Config.get(key, undefined);
|
||||
if (index === null || index === undefined) continue;
|
||||
if (index >= 0 && index < onboarding[key].length - 1) return key;
|
||||
if (
|
||||
index >= 0 &&
|
||||
index < onboarding[key as keyof typeof onboarding].length - 1
|
||||
)
|
||||
return key as keyof typeof onboarding;
|
||||
}
|
||||
}
|
||||
|
||||
function OnboardingDialog({ onClose: _onClose, type }) {
|
||||
const [step, setStep] = usePersistentState(type, 0);
|
||||
const steps = onboarding[type];
|
||||
type OnboardingDialogProps = BaseDialogProps<boolean> & {
|
||||
type: keyof typeof onboarding;
|
||||
};
|
||||
export const OnboardingDialog = DialogManager.register(
|
||||
function OnboardingDialog({
|
||||
onClose: _onClose,
|
||||
type
|
||||
}: OnboardingDialogProps) {
|
||||
const [step, setStep] = usePersistentState(type, 0);
|
||||
console.log("STEPS", type);
|
||||
const steps = onboarding[type];
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
Config.set(type, steps.length);
|
||||
_onClose();
|
||||
}, [_onClose, type, steps]);
|
||||
const onClose = useCallback(
|
||||
(result: boolean) => {
|
||||
Config.set(type, steps.length);
|
||||
_onClose(result);
|
||||
},
|
||||
[_onClose, type, steps]
|
||||
);
|
||||
|
||||
const onNext = useCallback(() => {
|
||||
if (step === steps.length - 1) onClose();
|
||||
else setStep((s) => ++s);
|
||||
}, [onClose, setStep, step, steps.length]);
|
||||
const onNext = useCallback(() => {
|
||||
if (step === steps.length - 1) onClose(true);
|
||||
else setStep((s) => ++s);
|
||||
}, [onClose, setStep, step, steps.length]);
|
||||
|
||||
if (!steps || !steps[step] || !type) {
|
||||
onClose();
|
||||
return null;
|
||||
}
|
||||
if (!steps || !steps[step] || !type) {
|
||||
onClose(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
image,
|
||||
component: Component,
|
||||
buttonText
|
||||
} = steps[step];
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
image,
|
||||
component: Component,
|
||||
buttonText
|
||||
} = steps[step];
|
||||
|
||||
return (
|
||||
<Dialog isOpen={true} width={500}>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
>
|
||||
{image}
|
||||
<Text variant={"heading"} mt={2}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
variant={"body"}
|
||||
return (
|
||||
<Dialog isOpen={true} width={500}>
|
||||
<Flex
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
maxWidth: "70%",
|
||||
color: "var(--paragraph-secondary)"
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
overflowY: "auto"
|
||||
}}
|
||||
>
|
||||
{subtitle}
|
||||
</Text>
|
||||
{Component && <Component onClose={onClose} onNext={onNext} />}
|
||||
{buttonText && (
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ borderRadius: 50, px: 30, mb: 4, mt: Component ? 0 : 4 }}
|
||||
onClick={onNext}
|
||||
{image}
|
||||
<Text variant={"heading"} mt={2}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
variant={"body"}
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
maxWidth: "70%",
|
||||
color: "var(--paragraph-secondary)"
|
||||
}}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default OnboardingDialog;
|
||||
{subtitle}
|
||||
</Text>
|
||||
{Component && (
|
||||
<Component onClose={() => onClose(true)} onNext={onNext} />
|
||||
)}
|
||||
{buttonText && (
|
||||
<Button
|
||||
variant="accent"
|
||||
sx={{ borderRadius: 50, px: 30, mb: 4, mt: Component ? 0 : 4 }}
|
||||
onClick={onNext}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
function JoinCause({ onNext }) {
|
||||
function JoinCause({ onNext }: { onNext: () => void }) {
|
||||
return (
|
||||
<Flex mb={4} sx={{ flexDirection: "column" }}>
|
||||
<Button
|
||||
as="a"
|
||||
href="https://discord.com/invite/zQBK97EE22"
|
||||
target="_blank"
|
||||
mt={4}
|
||||
variant="accent"
|
||||
sx={{ borderRadius: 50, alignSelf: "center", px: 30 }}
|
||||
onClick={() => onNext()}
|
||||
onClick={() => {
|
||||
window.open("https://go.notesnook.com/discord", "_blank");
|
||||
onNext();
|
||||
}}
|
||||
>
|
||||
Join the community
|
||||
</Button>
|
||||
@@ -235,7 +264,7 @@ const importers = [
|
||||
{ title: "Google Keep" },
|
||||
{ title: "Standard Notes" }
|
||||
];
|
||||
function Importer({ onClose }) {
|
||||
function Importer({ onClose }: { onClose: () => void }) {
|
||||
return (
|
||||
<Flex my={4} sx={{ flexDirection: "column" }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 1 }}>
|
||||
@@ -260,12 +289,13 @@ function Importer({ onClose }) {
|
||||
</Box>
|
||||
<Button
|
||||
as="a"
|
||||
href="https://importer.notesnook.com/"
|
||||
target="_blank"
|
||||
mt={4}
|
||||
variant="accent"
|
||||
sx={{ borderRadius: 50, alignSelf: "center", px: 30 }}
|
||||
onClick={() => onClose()}
|
||||
onClick={() => {
|
||||
window.open("https://importer.notesnook.com/", "_blank");
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Start importing now
|
||||
</Button>
|
||||
@@ -315,7 +345,6 @@ function Support() {
|
||||
<Button
|
||||
key={channel.key}
|
||||
as="a"
|
||||
href={channel.url}
|
||||
variant={"icon"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -323,6 +352,7 @@ function Support() {
|
||||
borderRadius: "default",
|
||||
alignItems: "center"
|
||||
}}
|
||||
onClick={() => window.open(channel.url)}
|
||||
>
|
||||
<channel.icon size={16} />
|
||||
<Text variant={"body"} ml={1}>
|
||||
@@ -335,8 +365,8 @@ function Support() {
|
||||
}
|
||||
|
||||
const themes = [
|
||||
{ key: "light", name: "Light", image: LightUI },
|
||||
{ key: "dark", name: "Dark", image: DarkUI }
|
||||
{ key: "light" as const, name: "Light", image: LightUI },
|
||||
{ key: "dark" as const, name: "Dark", image: DarkUI }
|
||||
];
|
||||
|
||||
function ThemeSelector() {
|
||||
@@ -436,9 +466,9 @@ function CrossPlatform() {
|
||||
);
|
||||
}
|
||||
|
||||
function TrialOffer({ onClose }) {
|
||||
const [error, setError] = useState();
|
||||
const [loading, setLoading] = useState();
|
||||
function TrialOffer({ onClose }: { onClose: () => void }) {
|
||||
const [error, setError] = useState<string>();
|
||||
const [loading, setLoading] = useState<boolean>();
|
||||
return (
|
||||
<Flex
|
||||
my={4}
|
||||
@@ -469,7 +499,7 @@ function TrialOffer({ onClose }) {
|
||||
sx={{ borderRadius: 50, alignSelf: "center", mr: 2, width: "40%" }}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
showBuyDialog("monthly", "TRIAL2PRO");
|
||||
BuyDialog.show({ plan: "monthly", couponCode: "TRIAL2PRO" });
|
||||
}}
|
||||
>
|
||||
Upgrade now
|
||||
@@ -493,7 +523,9 @@ function TrialOffer({ onClose }) {
|
||||
if (result) onClose();
|
||||
} catch (e) {
|
||||
setError(
|
||||
`Could not activate trial. Please try again. Error: ${e.message}`
|
||||
`Could not activate trial. Please try again. Error: ${
|
||||
(e as Error).message
|
||||
}`
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -22,25 +22,25 @@ import { Box, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import Field, { FieldProps } from "../components/field";
|
||||
import { Checkbox, Label } from "@theme-ui/components";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import { mdToHtml } from "../utils/md";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
type Check = { text: string; default?: boolean };
|
||||
export type PasswordDialogProps<
|
||||
TInputId extends string,
|
||||
TCheckId extends string
|
||||
> = {
|
||||
> = BaseDialogProps<boolean | Record<TCheckId, boolean>> & {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
message?: string;
|
||||
inputs: Record<TInputId, FieldProps>;
|
||||
validate: (passwords: Record<TInputId, string>) => Promise<boolean>;
|
||||
checks?: Record<TCheckId, Check>;
|
||||
onClose: Perform<boolean | Record<TCheckId, boolean>>;
|
||||
};
|
||||
function PasswordDialog<TInputId extends string, TCheckId extends string>(
|
||||
props: PasswordDialogProps<TInputId, TCheckId>
|
||||
) {
|
||||
const PasswordDialog = DialogManager.register(function PasswordDialog<
|
||||
TInputId extends string,
|
||||
TCheckId extends string
|
||||
>(props: PasswordDialogProps<TInputId, TCheckId>) {
|
||||
const { checks, inputs, message, validate, onClose } = props;
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -150,5 +150,13 @@ function PasswordDialog<TInputId extends string, TCheckId extends string>(
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
export function showPasswordDialog<
|
||||
TInputId extends string,
|
||||
TCheckId extends string
|
||||
>(props: Omit<PasswordDialogProps<TInputId, TCheckId>, "onClose">) {
|
||||
return PasswordDialog.show(props) as Promise<
|
||||
string extends TCheckId ? boolean : false | Record<TCheckId, boolean>
|
||||
>;
|
||||
}
|
||||
export default PasswordDialog;
|
||||
|
||||
@@ -20,21 +20,32 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Flex, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
function ProgressDialog(props) {
|
||||
const [{ current, total, text }, setProgress] = useState({
|
||||
current: 0,
|
||||
total: 1,
|
||||
type Progress = {
|
||||
total?: number;
|
||||
current?: number;
|
||||
text: string;
|
||||
};
|
||||
type ProgressDialogProps<T> = BaseDialogProps<T | Error> & {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
action: (report: (progress: Progress) => void) => T;
|
||||
};
|
||||
export const ProgressDialog = DialogManager.register(function ProgressDialog<T>(
|
||||
props: ProgressDialogProps<T>
|
||||
) {
|
||||
const [{ current = 0, total = 1, text }, setProgress] = useState<Progress>({
|
||||
text: ""
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
props.onDone(await props.action(setProgress));
|
||||
props.onClose(await props.action(setProgress));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
props.onDone(e);
|
||||
props.onClose(e as Error);
|
||||
}
|
||||
})();
|
||||
}, [props]);
|
||||
@@ -69,5 +80,4 @@ function ProgressDialog(props) {
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default ProgressDialog;
|
||||
});
|
||||
@@ -18,19 +18,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useRef } from "react";
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Field from "../components/field";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
export type PromptDialogProps = {
|
||||
onClose: Perform;
|
||||
export type PromptDialogProps = BaseDialogProps<false | string> & {
|
||||
title: string;
|
||||
onSave: (text: string) => void;
|
||||
description?: string;
|
||||
defaultValue?: string;
|
||||
};
|
||||
|
||||
export default function Prompt(props: PromptDialogProps) {
|
||||
export const PromptDialog = DialogManager.register(function PromptDialog(
|
||||
props: PromptDialogProps
|
||||
) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
return (
|
||||
<Dialog
|
||||
@@ -40,7 +40,7 @@ export default function Prompt(props: PromptDialogProps) {
|
||||
onClose={() => props.onClose(false)}
|
||||
positiveButton={{
|
||||
text: "Done",
|
||||
onClick: () => props.onSave(inputRef.current?.value || "")
|
||||
onClick: () => props.onClose(inputRef.current?.value || "")
|
||||
}}
|
||||
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
|
||||
>
|
||||
@@ -49,9 +49,9 @@ export default function Prompt(props: PromptDialogProps) {
|
||||
defaultValue={props.defaultValue}
|
||||
autoFocus
|
||||
onKeyUp={(e) => {
|
||||
if (e.key == "Enter") props.onSave(inputRef.current?.value || "");
|
||||
if (e.key == "Enter") props.onClose(inputRef.current?.value || "");
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Text, Flex, Button } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { db } from "../common/db";
|
||||
import Logo from "../assets/notesnook-logo.png";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { Suspense } from "react";
|
||||
import Config from "../utils/config";
|
||||
import FileSaver from "file-saver";
|
||||
import { ErrorText } from "../components/error-text";
|
||||
|
||||
const QRCode = React.lazy(() => import("../re-exports/react-qrcode-logo"));
|
||||
|
||||
function RecoveryKeyDialog(props) {
|
||||
const [key, setKey] = useState();
|
||||
const [email, setEmail] = useState();
|
||||
const [copyText, setCopyText] = useState("Copy to clipboard");
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { email } = await db.user.getUser();
|
||||
const { key } = await db.user.getEncryptionKey();
|
||||
setEmail(email);
|
||||
setKey(key);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
testId="recovery-key-dialog"
|
||||
isOpen={true}
|
||||
title="Backup your recovery key"
|
||||
width={400}
|
||||
positiveButton={{
|
||||
text: "I have backed up my key",
|
||||
onClick: () => {
|
||||
Config.set("recoveryKeyBackupDate", Date.now());
|
||||
props.onDone();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ overflow: "hidden", flex: 1, flexDirection: "column" }}>
|
||||
<Flex sx={{ overflowY: "auto", flexDirection: "column" }}>
|
||||
<ErrorText
|
||||
error="In case you forget your password, your recovery key is the only way to recover your data."
|
||||
mt={0}
|
||||
/>
|
||||
<Text
|
||||
data-test-id="recovery-key"
|
||||
className="selectable"
|
||||
mt={2}
|
||||
bg="var(--background-secondary)"
|
||||
p={2}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
overflowWrap: "anywhere",
|
||||
fontSize: "body",
|
||||
fontFamily: "monospace",
|
||||
color: "paragraph"
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</Text>
|
||||
<Flex
|
||||
mt={4}
|
||||
sx={{ alignItems: "center", justifyContent: "space-around" }}
|
||||
>
|
||||
<Suspense fallback={<div />}>
|
||||
<QRCode
|
||||
value={key}
|
||||
logoImage={Logo}
|
||||
logoWidth={40}
|
||||
logoHeight={40}
|
||||
ecLevel={"M"}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
mt={1}
|
||||
className="copyKey"
|
||||
onClick={async () => {
|
||||
writeText(key)
|
||||
.then(() => {
|
||||
setCopyText("Copied!");
|
||||
setTimeout(() => {
|
||||
setCopyText("Copy to clipboard");
|
||||
}, 2000);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Error while copying text.", e);
|
||||
});
|
||||
}}
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
{copyText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
mt={1}
|
||||
onClick={async () => {
|
||||
const qrcode = document.getElementById("react-qrcode-logo");
|
||||
qrcode.toBlob((blob) => {
|
||||
FileSaver.saveAs(
|
||||
blob,
|
||||
`${email}-notesnook-recoverykey.png`
|
||||
);
|
||||
});
|
||||
}}
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
Download QR Code
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
mt={1}
|
||||
onClick={() => {
|
||||
FileSaver.saveAs(
|
||||
new Blob([Buffer.from(key)]),
|
||||
`${email}-notesnook-recoverykey.txt`
|
||||
);
|
||||
}}
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
Download file
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default RecoveryKeyDialog;
|
||||
166
apps/web/src/dialogs/recovery-key-dialog.tsx
Normal file
166
apps/web/src/dialogs/recovery-key-dialog.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Text, Flex, Button } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { db } from "../common/db";
|
||||
import Logo from "../assets/notesnook-logo.png";
|
||||
import { writeText } from "clipboard-polyfill";
|
||||
import { Suspense } from "react";
|
||||
import Config from "../utils/config";
|
||||
import FileSaver from "file-saver";
|
||||
import { ErrorText } from "../components/error-text";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { usePromise } from "@notesnook/common";
|
||||
import { Loader } from "../components/loader";
|
||||
import { showToast } from "../utils/toast";
|
||||
|
||||
const QRCode = React.lazy(() => import("../re-exports/react-qrcode-logo"));
|
||||
type RecoveryKeyDialogProps = BaseDialogProps<false>;
|
||||
export const RecoveryKeyDialog = DialogManager.register(
|
||||
function RecoveryKeyDialog(props: RecoveryKeyDialogProps) {
|
||||
const email = usePromise(() =>
|
||||
db.user.getUser().then((user) => user?.email)
|
||||
);
|
||||
const key = usePromise(() =>
|
||||
db.user.getEncryptionKey().then((key) => key?.key)
|
||||
);
|
||||
const [copyText, setCopyText] = useState("Copy to clipboard");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
testId="recovery-key-dialog"
|
||||
isOpen={true}
|
||||
title="Backup your recovery key"
|
||||
width={400}
|
||||
positiveButton={{
|
||||
text: "I have backed up my key",
|
||||
onClick: () => {
|
||||
Config.set("recoveryKeyBackupDate", Date.now());
|
||||
props.onClose(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{key.status !== "fulfilled" ? (
|
||||
<Loader title="Getting your encryption key..." />
|
||||
) : (
|
||||
<Flex sx={{ overflow: "hidden", flex: 1, flexDirection: "column" }}>
|
||||
<Flex sx={{ overflowY: "auto", flexDirection: "column" }}>
|
||||
<ErrorText
|
||||
error="In case you forget your password, your recovery key is the only way to recover your data."
|
||||
mt={0}
|
||||
/>
|
||||
<Text
|
||||
data-test-id="recovery-key"
|
||||
className="selectable"
|
||||
mt={2}
|
||||
bg="var(--background-secondary)"
|
||||
p={2}
|
||||
sx={{
|
||||
borderRadius: "default",
|
||||
overflowWrap: "anywhere",
|
||||
fontSize: "body",
|
||||
fontFamily: "monospace",
|
||||
color: "paragraph"
|
||||
}}
|
||||
>
|
||||
{key.value}
|
||||
</Text>
|
||||
<Flex
|
||||
mt={4}
|
||||
sx={{ alignItems: "center", justifyContent: "space-around" }}
|
||||
>
|
||||
<Suspense fallback={<div />}>
|
||||
<QRCode
|
||||
value={key.value}
|
||||
logoImage={Logo}
|
||||
logoWidth={40}
|
||||
logoHeight={40}
|
||||
ecLevel={"M"}
|
||||
/>
|
||||
</Suspense>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
mt={1}
|
||||
className="copyKey"
|
||||
onClick={async () => {
|
||||
if (!key.value)
|
||||
return showToast("error", "No encryption key found.");
|
||||
|
||||
writeText(key.value)
|
||||
.then(() => {
|
||||
setCopyText("Copied!");
|
||||
setTimeout(() => {
|
||||
setCopyText("Copy to clipboard");
|
||||
}, 2000);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Error while copying text.", e);
|
||||
});
|
||||
}}
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
{copyText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
mt={1}
|
||||
onClick={async () => {
|
||||
const qrcode = document.getElementById(
|
||||
"react-qrcode-logo"
|
||||
) as HTMLCanvasElement | null;
|
||||
qrcode?.toBlob((blob) => {
|
||||
blob
|
||||
? FileSaver.saveAs(
|
||||
blob,
|
||||
`${email}-notesnook-recoverykey.png`
|
||||
)
|
||||
: null;
|
||||
});
|
||||
}}
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
Download QR Code
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
mt={1}
|
||||
onClick={() => {
|
||||
if (!key.value)
|
||||
return showToast("error", "No encryption key found.");
|
||||
FileSaver.saveAs(
|
||||
new Blob([Buffer.from(key.value)]),
|
||||
`${email}-notesnook-recoverykey.txt`
|
||||
);
|
||||
}}
|
||||
sx={{ fontSize: "body" }}
|
||||
>
|
||||
Download file
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -27,9 +27,12 @@ import {
|
||||
Backup,
|
||||
Vault,
|
||||
PDF,
|
||||
Edit
|
||||
Edit,
|
||||
Icon
|
||||
} from "../components/icons";
|
||||
import { showBuyDialog } from "../common/dialog-controller";
|
||||
import { BuyDialog } from "./buy-dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import Config from "../utils/config";
|
||||
|
||||
const features = [
|
||||
{ icon: Sync, title: "Instant private sync" },
|
||||
@@ -41,8 +44,22 @@ const features = [
|
||||
{ icon: PDF, title: "Export to PDF" },
|
||||
{ icon: Edit, title: "Rich text editor" }
|
||||
];
|
||||
|
||||
type Reminder = {
|
||||
title: string;
|
||||
description: string;
|
||||
offer: {
|
||||
title: JSX.Element;
|
||||
subtitle?: string;
|
||||
};
|
||||
features: { icon: Icon; title: string }[];
|
||||
cta: {
|
||||
title: string;
|
||||
action: () => Promise<void> | void;
|
||||
};
|
||||
};
|
||||
// TODO create trial expiry coupon codes
|
||||
const reminders = {
|
||||
const reminders: Record<string, Reminder> = {
|
||||
trialexpired: {
|
||||
title: "Your free trial has expired",
|
||||
description: "But we have a special offer just for you!",
|
||||
@@ -70,7 +87,7 @@ const reminders = {
|
||||
title: "Subscribe now",
|
||||
action: () => {
|
||||
setTimeout(async () => {
|
||||
await showBuyDialog("yearly", "TRIAL2PRO");
|
||||
await BuyDialog.show({ plan: "yearly", couponCode: "TRIAL2PRO" });
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -91,17 +108,23 @@ const reminders = {
|
||||
title: "Upgrade now",
|
||||
action: () => {
|
||||
setTimeout(async () => {
|
||||
await showBuyDialog();
|
||||
await BuyDialog.show({});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function ReminderDialog(props) {
|
||||
type ReminderDialogProps = BaseDialogProps<boolean> & {
|
||||
reminderKey: keyof typeof reminders;
|
||||
};
|
||||
export const ReminderDialog = DialogManager.register(function ReminderDialog(
|
||||
props: ReminderDialogProps
|
||||
) {
|
||||
const { reminderKey } = props;
|
||||
const reminder = reminders[reminderKey];
|
||||
if (!reminder) return null;
|
||||
if (Config.get(reminderKey, false)) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -111,7 +134,6 @@ function ReminderDialog(props) {
|
||||
onClose={() => props.onClose(false)}
|
||||
showCloseButton
|
||||
description={reminder.description}
|
||||
alignment="center"
|
||||
positiveButton={{
|
||||
text: <Flex>{reminder.cta.title}</Flex>,
|
||||
onClick: async () => {
|
||||
@@ -120,7 +142,6 @@ function ReminderDialog(props) {
|
||||
props.onClose(true);
|
||||
}
|
||||
}}
|
||||
footer={reminder.footer}
|
||||
>
|
||||
<Flex
|
||||
mt={2}
|
||||
@@ -175,5 +196,4 @@ function ReminderDialog(props) {
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export default ReminderDialog;
|
||||
});
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Perform } from "../common/dialog-controller";
|
||||
import Dialog from "../components/dialog";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { db } from "../common/db";
|
||||
@@ -26,9 +25,9 @@ import IconTag from "../components/icon-tag";
|
||||
import { Clock, Refresh } from "../components/icons";
|
||||
import Note from "../components/note";
|
||||
import { getFormattedReminderTime, usePromise } from "@notesnook/common";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
|
||||
export type ReminderPreviewDialogProps = {
|
||||
onClose: Perform;
|
||||
export type ReminderPreviewDialogProps = BaseDialogProps<false> & {
|
||||
reminder: Reminder;
|
||||
};
|
||||
|
||||
@@ -54,82 +53,89 @@ const SNOOZE_TIMES = [
|
||||
{ id: "1-hour", title: "1 hour", interval: 60 * 60 * 1000 }
|
||||
];
|
||||
|
||||
export default function ReminderPreviewDialog(
|
||||
props: ReminderPreviewDialogProps
|
||||
) {
|
||||
const { reminder } = props;
|
||||
const referencedNotes = usePromise(
|
||||
() =>
|
||||
db.relations.to({ id: reminder.id, type: "reminder" }, "note").resolve(),
|
||||
[reminder.id]
|
||||
);
|
||||
export const ReminderPreviewDialog = DialogManager.register(
|
||||
function ReminderPreviewDialog(props: ReminderPreviewDialogProps) {
|
||||
const { reminder } = props;
|
||||
const referencedNotes = usePromise(
|
||||
() =>
|
||||
db.relations
|
||||
.to({ id: reminder.id, type: "reminder" }, "note")
|
||||
.resolve(),
|
||||
[reminder.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={reminder.title}
|
||||
description={reminder.description}
|
||||
onClose={() => props.onClose(false)}
|
||||
negativeButton={{
|
||||
text: "Close",
|
||||
onClick: () => props.onClose(false)
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
mb: 2
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={reminder.title}
|
||||
description={reminder.description}
|
||||
onClose={() => props.onClose(false)}
|
||||
negativeButton={{
|
||||
text: "Close",
|
||||
onClick: () => props.onClose(false)
|
||||
}}
|
||||
>
|
||||
{reminder.mode === "repeat" && reminder.recurringMode && (
|
||||
<IconTag
|
||||
icon={Refresh}
|
||||
text={RECURRING_MODE_MAP[reminder.recurringMode]}
|
||||
/>
|
||||
)}
|
||||
<IconTag icon={Clock} text={getFormattedReminderTime(reminder)} />
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
{reminder.mode === "repeat" && reminder.recurringMode && (
|
||||
<IconTag
|
||||
icon={Refresh}
|
||||
text={RECURRING_MODE_MAP[reminder.recurringMode]}
|
||||
/>
|
||||
)}
|
||||
<IconTag icon={Clock} text={getFormattedReminderTime(reminder)} />
|
||||
</Flex>
|
||||
|
||||
<Text variant="body">Remind me in:</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
my: 1,
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{SNOOZE_TIMES.map((time) => (
|
||||
<Button
|
||||
key={time.id}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
db.reminders.add({
|
||||
id: reminder.id,
|
||||
snoozeUntil: Date.now() + time.interval
|
||||
});
|
||||
props.onClose(false);
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 100,
|
||||
py: 1,
|
||||
px: 2,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
{time.title}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{referencedNotes &&
|
||||
referencedNotes.status === "fulfilled" &&
|
||||
referencedNotes.value.length > 0 && (
|
||||
<>
|
||||
<Text variant="body">References:</Text>
|
||||
{referencedNotes.value.map((item, index) => (
|
||||
<Note key={item.id} item={item} date={item.dateCreated} compact />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
<Text variant="body">Remind me in:</Text>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
my: 1,
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
{SNOOZE_TIMES.map((time) => (
|
||||
<Button
|
||||
key={time.id}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
db.reminders.add({
|
||||
id: reminder.id,
|
||||
snoozeUntil: Date.now() + time.interval
|
||||
});
|
||||
props.onClose(false);
|
||||
}}
|
||||
sx={{
|
||||
borderRadius: 100,
|
||||
py: 1,
|
||||
px: 2,
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
{time.title}
|
||||
</Button>
|
||||
))}
|
||||
</Flex>
|
||||
{referencedNotes &&
|
||||
referencedNotes.status === "fulfilled" &&
|
||||
referencedNotes.value.length > 0 && (
|
||||
<>
|
||||
<Text variant="body">References:</Text>
|
||||
{referencedNotes.value.map((item, index) => (
|
||||
<Note
|
||||
key={item.id}
|
||||
item={item}
|
||||
date={item.dateCreated}
|
||||
compact
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -19,21 +19,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { SettingComponent, SettingsGroup } from "./types";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import {
|
||||
showPasswordDialog,
|
||||
showPromptDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { showPasswordDialog } from "../../dialogs/password-dialog";
|
||||
import {
|
||||
CredentialType,
|
||||
CredentialWithSecret,
|
||||
CredentialWithoutSecret,
|
||||
useKeyStore
|
||||
useKeyStore,
|
||||
wrongCredentialError
|
||||
} from "../../interfaces/key-store";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { WebAuthn } from "../../utils/webauthn";
|
||||
import { generatePassword } from "../../utils/password-generator";
|
||||
import { verifyAccount } from "../../common";
|
||||
import { Checkmark } from "../../components/icons";
|
||||
import { PromptDialog } from "../prompt";
|
||||
|
||||
export const AppLockSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -317,7 +316,7 @@ async function registerCredential(type: CredentialType) {
|
||||
const user = useUserStore.getState().user;
|
||||
const username =
|
||||
user?.email ||
|
||||
(await showPromptDialog({
|
||||
(await PromptDialog.show({
|
||||
title: "Enter your username",
|
||||
description:
|
||||
"This username will be used to distinguish between different credentials in your security key. Make sure it is unique."
|
||||
@@ -412,7 +411,8 @@ async function authenticateAppLock() {
|
||||
if (!defaultCredential) {
|
||||
return verifyAccount();
|
||||
}
|
||||
return !!(await verifyCredential(defaultCredential, (c) =>
|
||||
useKeyStore.getState().verifyCredential(c)
|
||||
));
|
||||
return !!(await verifyCredential(defaultCredential, async (c) => {
|
||||
if (!(await useKeyStore.getState().verifyCredential(c)))
|
||||
throw new Error(wrongCredentialError(c));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -20,14 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { SettingsGroup } from "./types";
|
||||
import { useStore as useUserStore } from "../../stores/user-store";
|
||||
import { createBackup, verifyAccount } from "../../common";
|
||||
import {
|
||||
show2FARecoveryCodesDialog,
|
||||
showMultifactorDialog,
|
||||
showPasswordDialog,
|
||||
showRecoveryKeyDialog
|
||||
} from "../../common/dialog-controller";
|
||||
import { showPasswordDialog } from "../../dialogs/password-dialog";
|
||||
import { db } from "../../common/db";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { RecoveryCodesDialog } from "../mfa/recovery-code-dialog";
|
||||
import { MultifactorDialog } from "../mfa/multi-factor-dialog";
|
||||
import { RecoveryKeyDialog } from "../recovery-key-dialog";
|
||||
|
||||
export const AuthenticationSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -74,7 +72,7 @@ If this process is interrupted, there is a high chance of data corruption so **p
|
||||
});
|
||||
if (result) {
|
||||
showToast("success", "Account password changed!");
|
||||
await showRecoveryKeyDialog();
|
||||
await RecoveryKeyDialog.show({});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +108,7 @@ If this process is interrupted, there is a high chance of data corruption so **p
|
||||
title: "Change",
|
||||
action: async () => {
|
||||
if (await verifyAccount()) {
|
||||
await showMultifactorDialog();
|
||||
await MultifactorDialog.show({});
|
||||
await useUserStore.getState().refreshUser();
|
||||
}
|
||||
},
|
||||
@@ -135,9 +133,10 @@ If this process is interrupted, there is a high chance of data corruption so **p
|
||||
variant: "secondary",
|
||||
action: async () => {
|
||||
if (await verifyAccount()) {
|
||||
await showMultifactorDialog(
|
||||
useUserStore.getState().user?.mfa.primaryMethod || "email"
|
||||
);
|
||||
await MultifactorDialog.show({
|
||||
primaryMethod:
|
||||
useUserStore.getState().user?.mfa.primaryMethod || "email"
|
||||
});
|
||||
await useUserStore.getState().refreshUser();
|
||||
}
|
||||
}
|
||||
@@ -157,9 +156,10 @@ If this process is interrupted, there is a high chance of data corruption so **p
|
||||
variant: "secondary",
|
||||
action: async () => {
|
||||
if (await verifyAccount()) {
|
||||
await show2FARecoveryCodesDialog(
|
||||
useUserStore.getState().user?.mfa.primaryMethod || "email"
|
||||
);
|
||||
await RecoveryCodesDialog.show({
|
||||
primaryMethod:
|
||||
useUserStore.getState().user?.mfa.primaryMethod || "email"
|
||||
});
|
||||
await useUserStore.getState().refreshUser();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ import { useCallback, useMemo, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { SUBSCRIPTION_STATUS } from "../../../common/constants";
|
||||
import { db } from "../../../common/db";
|
||||
import { confirm, showBuyDialog } from "../../../common/dialog-controller";
|
||||
import { TaskManager } from "../../../common/task-manager";
|
||||
import { showToast } from "../../../utils/toast";
|
||||
import { Loading } from "../../../components/icons";
|
||||
import { Features } from "../../buy-dialog/features";
|
||||
import { ConfirmDialog } from "../../confirm";
|
||||
import { BuyDialog } from "../../buy-dialog";
|
||||
|
||||
const PROVIDER_MAP = {
|
||||
0: "Streetwriters",
|
||||
@@ -139,7 +140,7 @@ export function SubscriptionStatus() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const cancelSubscription = await confirm({
|
||||
const cancelSubscription = await ConfirmDialog.show({
|
||||
title: "Cancel subscription?",
|
||||
message:
|
||||
"Cancelling your subscription will automatically downgrade you to the Basic plan at the end of your billing period. You will have to resubscribe to continue using the Pro features.",
|
||||
@@ -169,7 +170,7 @@ export function SubscriptionStatus() {
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
const refundSubscription = await confirm({
|
||||
const refundSubscription = await ConfirmDialog.show({
|
||||
title: "Request refund?",
|
||||
message:
|
||||
"You will only be issued a refund if you are eligible as per our refund policy. Your account will be immediately downgraded to Basic and your funds will be transferred to your account within 24 hours.",
|
||||
@@ -199,7 +200,7 @@ export function SubscriptionStatus() {
|
||||
) : null}
|
||||
{!isPro && (
|
||||
<>
|
||||
<Button variant="accent" onClick={() => showBuyDialog()}>
|
||||
<Button variant="accent" onClick={() => BuyDialog.show({})}>
|
||||
{isProCancelled ? "Resubscribe" : "Upgrade to Pro"}
|
||||
</Button>
|
||||
{isBasic && (
|
||||
|
||||
@@ -35,12 +35,12 @@ import {
|
||||
ThemesTRPC
|
||||
} from "../../../common/themes-router";
|
||||
import { ThemeMetadata } from "@notesnook/themes-server";
|
||||
import { showThemeDetails } from "../../../common/dialog-controller";
|
||||
import { ThemePreview } from "../../../components/theme-preview";
|
||||
import { Loader } from "../../../components/loader";
|
||||
import { showToast } from "../../../utils/toast";
|
||||
import { showFilePicker, readFile } from "../../../utils/file-picker";
|
||||
import { VirtualizedGrid } from "../../../components/virtualized-grid";
|
||||
import { ThemeDetailsDialog } from "../../theme-details-dialog";
|
||||
|
||||
const ThemesClient = ThemesTRPC.createClient({
|
||||
links: [
|
||||
@@ -189,10 +189,12 @@ function ThemesList() {
|
||||
if (error) return showToast("error", error);
|
||||
|
||||
if (
|
||||
await showThemeDetails({
|
||||
...theme,
|
||||
totalInstalls: 0,
|
||||
previewColors: getPreviewColors(theme)
|
||||
await ThemeDetailsDialog.show({
|
||||
theme: {
|
||||
...theme,
|
||||
totalInstalls: 0,
|
||||
previewColors: getPreviewColors(theme)
|
||||
}
|
||||
})
|
||||
) {
|
||||
setCurrentTheme(theme);
|
||||
@@ -266,7 +268,7 @@ function ThemeItem(props: ThemeItemProps) {
|
||||
}
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (await showThemeDetails(theme)) {
|
||||
if (await ThemeDetailsDialog.show({ theme })) {
|
||||
await setTheme(theme);
|
||||
}
|
||||
}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user