mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
16 Commits
fix/317
...
mobile/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12a7b36562 | ||
|
|
3ea4d0326d | ||
|
|
d781ebb52a | ||
|
|
e3213c6d21 | ||
|
|
fc9c029752 | ||
|
|
8c8af1b3f7 | ||
|
|
fd25621aa7 | ||
|
|
37361c575a | ||
|
|
2596a672d1 | ||
|
|
6c45cfb681 | ||
|
|
072cb9b4d2 | ||
|
|
c9467bad07 | ||
|
|
8d2329898a | ||
|
|
c581e706d2 | ||
|
|
76ea082536 | ||
|
|
cd2e6d152b |
31
.github/workflows/android.preview.firebase.yml
vendored
31
.github/workflows/android.preview.firebase.yml
vendored
@@ -61,6 +61,37 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Generate dependency hash
|
||||
id: dep-hash
|
||||
run: |
|
||||
DEPS_HASH=$(node -e "
|
||||
const pkg = require('./apps/mobile/package.json');
|
||||
const crypto = require('crypto');
|
||||
const depsStr = JSON.stringify(pkg.dependencies);
|
||||
const hash = crypto.createHash('md5').update(depsStr).digest('hex');
|
||||
console.log(hash);
|
||||
")
|
||||
echo "hash=$DEPS_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Android build files
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
apps/mobile/android/.gradle
|
||||
apps/mobile/android/build
|
||||
apps/mobile/android/app/build
|
||||
apps/mobile/android/app/.cxx
|
||||
node_modules/**/android/build
|
||||
node_modules/**/android/.cxx
|
||||
node_modules/**/android/generated
|
||||
node_modules/**/.gradle
|
||||
node_modules/**/.cxx
|
||||
key: android-build-${{ runner.os }}-deps-${{ steps.dep-hash.outputs.hash }}
|
||||
restore-keys: |
|
||||
android-build-${{ runner.os }}-
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
|
||||
155
.github/workflows/android.preview.firebasetest.yml
vendored
Normal file
155
.github/workflows/android.preview.firebasetest.yml
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
name: Notesnook Android Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
branches: [master, beta]
|
||||
paths:
|
||||
- "apps/mobile/**"
|
||||
- "packages/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/android.preview.firebase.yml"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
STAGING_BUILD: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: false
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: true
|
||||
docker-images: true
|
||||
swap-storage: true
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Generate dependency hash
|
||||
id: dep-hash
|
||||
run: |
|
||||
DEPS_HASH=$(node -e "
|
||||
const pkg = require('./apps/mobile/package.json');
|
||||
const crypto = require('crypto');
|
||||
const depsStr = JSON.stringify(pkg.dependencies);
|
||||
const hash = crypto.createHash('md5').update(depsStr).digest('hex');
|
||||
console.log(hash);
|
||||
")
|
||||
echo "hash=$DEPS_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Android build files
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
apps/mobile/android/.gradle
|
||||
apps/mobile/android/build
|
||||
apps/mobile/android/app/build
|
||||
apps/mobile/android/app/.cxx
|
||||
node_modules/**/android/build
|
||||
node_modules/**/android/.cxx
|
||||
node_modules/**/android/generated
|
||||
node_modules/**/.gradle
|
||||
node_modules/**/.cxx
|
||||
key: android-build-${{ runner.os }}-deps-${{ steps.dep-hash.outputs.hash }}
|
||||
restore-keys: |
|
||||
android-build-${{ runner.os }}-
|
||||
|
||||
- name: Make Gradlew Executable
|
||||
run: cd apps/mobile/android && chmod +x ./gradlew
|
||||
|
||||
- name: Get build number
|
||||
id: build-number
|
||||
run: echo "timestamp=$(($(date +%s) - 1774851180 ))" >> $GITHUB_OUTPUT
|
||||
|
||||
# - name: Check for typescript errors
|
||||
# run: |
|
||||
# npm run tx mobile:build
|
||||
# cd apps/mobile
|
||||
# npx tsc --noEmit
|
||||
|
||||
- name: Build arm64-v8a apk
|
||||
run: |
|
||||
cd apps/mobile/android
|
||||
./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a -PstagingReleaseBuild=true -PprBuildNumber=${{ steps.build-number.outputs.timestamp }}
|
||||
|
||||
# - name: Get app version
|
||||
# id: package-version
|
||||
# uses: saionaro/extract-package-version@master
|
||||
# with:
|
||||
# path: apps/mobile
|
||||
|
||||
# - name: Publish to firebase CLI
|
||||
# id: firebase-output
|
||||
# uses: wzieba/Firebase-Distribution-Github-Action@v1
|
||||
# with:
|
||||
# appId: ${{secrets.FIREBASE_APP_ID}}
|
||||
# serviceCredentialsFileContent: ${{ secrets.QA_SERVICE_ACCOUNT }}
|
||||
# groups: testers
|
||||
# file: apps/mobile/android/app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||
# releaseNotes: Preview for https://github.com/streetwriters/notesnook/pull/${{github.event.number}}
|
||||
|
||||
# - name: Post or update PR comment
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# preview_url: ${{ steps.firebase-output.outputs.TESTING_URI }}
|
||||
# with:
|
||||
# script: |
|
||||
# const marker = '<!-- android-preview-comment -->';
|
||||
# const prNumber = context.issue.number;
|
||||
# const previewUrl = process.env.preview_url || '';
|
||||
# const body = `${marker}\n**Android App Preview**\n\n${previewUrl || 'Preview URL unavailable — check workflow logs.'}\n\nCommit: ${process.env.GITHUB_SHA}\n`;
|
||||
# const { data: comments } = await github.rest.issues.listComments({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# issue_number: prNumber,
|
||||
# });
|
||||
# const existing = comments.find(c => c.body && c.body.includes(marker));
|
||||
# if (existing) {
|
||||
# await github.rest.issues.updateComment({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# comment_id: existing.id,
|
||||
# body,
|
||||
# });
|
||||
# } else {
|
||||
# await github.rest.issues.createComment({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# issue_number: prNumber,
|
||||
# body,
|
||||
# });
|
||||
# }
|
||||
|
||||
# - name: Upload sourcemaps
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: sourcemaps
|
||||
# path: |
|
||||
# apps/mobile/android/app/build/generated/sourcemaps/**/*.map
|
||||
@@ -96,6 +96,7 @@ module.exports = {
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
afterPack: "./scripts/removeLocales.js",
|
||||
protocols: [{ name: "Notesnook", schemes: ["nn"] }],
|
||||
mac: {
|
||||
bundleVersion: "240",
|
||||
minimumSystemVersion: "10.12.0",
|
||||
@@ -181,6 +182,7 @@ module.exports = {
|
||||
icon: "assets/icons/app.icns",
|
||||
description: "Your private note taking space",
|
||||
executableName: linuxExecutableName,
|
||||
mimeTypes: ["x-scheme-handler/nn"],
|
||||
desktop: {
|
||||
desktopActions: {
|
||||
"new-note": {
|
||||
|
||||
@@ -24,21 +24,43 @@ import TypedEventEmitter from "typed-emitter";
|
||||
|
||||
export type AppEvents = {
|
||||
onCreateItem(name: "note" | "notebook" | "reminder"): void;
|
||||
onOpenLink(url: string): void;
|
||||
bridgeReady(): void;
|
||||
};
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
const typedEmitter = emitter as TypedEventEmitter<AppEvents>;
|
||||
let isBridgeReady = false;
|
||||
const pendingEvents: { name: string; args: unknown[] }[] = [];
|
||||
const _emitter = new EventEmitter();
|
||||
const emitter = _emitter as TypedEventEmitter<AppEvents>;
|
||||
const t = initTRPC.create();
|
||||
|
||||
export const bridgeRouter = t.router({
|
||||
onCreateItem: createSubscription("onCreateItem")
|
||||
onCreateItem: createSubscription("onCreateItem"),
|
||||
onOpenLink: createSubscription("onOpenLink"),
|
||||
ready: t.procedure.query(() => {
|
||||
isBridgeReady = true;
|
||||
if (pendingEvents.length > 0) {
|
||||
console.log(
|
||||
"Emitting pending events",
|
||||
pendingEvents.map((e) => e.name)
|
||||
);
|
||||
pendingEvents.forEach((event) => {
|
||||
emitter.emit(event.name as any, ...(event.args as any[]));
|
||||
});
|
||||
pendingEvents.length = 0;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
});
|
||||
|
||||
export const bridge: AppEvents = new Proxy({} as AppEvents, {
|
||||
get(_t, name) {
|
||||
if (typeof name === "symbol") return;
|
||||
return (...args: unknown[]) => {
|
||||
emitter.emit(name, ...args);
|
||||
if (!isBridgeReady) {
|
||||
pendingEvents.push({ name, args });
|
||||
return;
|
||||
}
|
||||
_emitter.emit(name, ...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -49,9 +71,9 @@ function createSubscription<TName extends keyof AppEvents>(eventName: TName) {
|
||||
const listener: AppEvents[TName] = (...args: any[]) => {
|
||||
emit.next(args[0]);
|
||||
};
|
||||
typedEmitter.addListener(eventName, listener);
|
||||
emitter.addListener(eventName, listener);
|
||||
return () => {
|
||||
typedEmitter.removeListener(eventName, listener);
|
||||
emitter.removeListener(eventName, listener);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,10 @@ setI18nGlobal(i18n);
|
||||
const appHostnames = isDevelopment()
|
||||
? ["localhost", "127.0.0.1"]
|
||||
: ["app.notesnook.com"];
|
||||
// Pending nn:// link to open once the window is ready (used on Windows/Linux
|
||||
// when the app is launched via the nn:// protocol for the first time).
|
||||
let pendingNNLink: string | undefined = findNNLink(process.argv);
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
console.log("Another instance is already running!");
|
||||
@@ -193,6 +197,11 @@ async function createWindow() {
|
||||
setupTray();
|
||||
setupJumplist();
|
||||
});
|
||||
|
||||
if (pendingNNLink) {
|
||||
bridge.onOpenLink(pendingNNLink);
|
||||
pendingNNLink = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
app.once("ready", async () => {
|
||||
@@ -212,6 +221,8 @@ app.once("ready", async () => {
|
||||
if (config.customDns) enableCustomDns();
|
||||
else disableCustomDns();
|
||||
|
||||
if (!MAC_APP_STORE) app.setAsDefaultProtocolClient("nn");
|
||||
|
||||
if (!isDevelopment()) registerProtocol();
|
||||
await createWindow();
|
||||
configureAutoUpdater();
|
||||
@@ -225,6 +236,12 @@ app.once("window-all-closed", () => {
|
||||
|
||||
app.on("second-instance", async (_ev, argv) => {
|
||||
if (!globalThis.window) return;
|
||||
const nnLink = findNNLink(argv);
|
||||
if (nnLink) {
|
||||
bridge.onOpenLink(nnLink);
|
||||
bringToFront();
|
||||
return;
|
||||
}
|
||||
const cliOptions = await parseArguments(argv);
|
||||
if (cliOptions.note) bridge.onCreateItem("note");
|
||||
if (cliOptions.notebook) bridge.onCreateItem("notebook");
|
||||
@@ -232,12 +249,29 @@ app.on("second-instance", async (_ev, argv) => {
|
||||
bringToFront();
|
||||
});
|
||||
|
||||
// macOS opens URLs via this event. The app may or may not be fully loaded yet.
|
||||
app.on("open-url", (event, url) => {
|
||||
event.preventDefault();
|
||||
if (!url.startsWith("nn://")) return;
|
||||
if (globalThis.window) {
|
||||
bridge.onOpenLink(url);
|
||||
bringToFront();
|
||||
} else {
|
||||
// Window not ready yet — store for when createWindow finishes loading.
|
||||
pendingNNLink = url;
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (globalThis.window === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
function findNNLink(argv: string[]): string | undefined {
|
||||
return argv.find((arg) => arg.startsWith("nn://"));
|
||||
}
|
||||
|
||||
function createURL(options: CLIOptions, path = "/") {
|
||||
const url = new URL(isDevelopment() ? "http://localhost:3000" : PROTOCOL_URL);
|
||||
|
||||
@@ -248,7 +282,7 @@ function createURL(options: CLIOptions, path = "/") {
|
||||
else if (typeof options.note === "string")
|
||||
url.hash = `/notes/${options.note}/edit`;
|
||||
else if (typeof options.notebook === "string")
|
||||
url.hash = `/notebooks/${options.notebook}`;
|
||||
url.pathname = `/notebooks/${options.notebook}`;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -155,6 +155,15 @@
|
||||
<data android:scheme="notesnook" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:label="Notesnook">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="nn" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.facebook.react.devsupport.DevSettingsActivity"
|
||||
|
||||
@@ -33,7 +33,7 @@ public class NotePreviewWidget extends AppWidgetProvider {
|
||||
intent.putExtra(OpenNoteId, note.getId());
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
intent.putExtra(RCTNNativeModule.IntentType, "OpenNote");
|
||||
intent.setData(Uri.parse("https://app.notesnook.com/open_note?id=" + note.getId()));
|
||||
intent.setData(Uri.parse("nn://note/" + note.getId()));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE, getActivityOptionsBundle());
|
||||
views.setOnClickPendingIntent(R.id.open_note, pendingIntent);
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ public class RCTNNativeModule extends ReactContextBaseJavaModule {
|
||||
return;
|
||||
}
|
||||
|
||||
String uri = "https://app.notesnook.com/open_" + type + "?id=" + id;
|
||||
String uri = "nn://" + type + "/" + id;
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(uri));
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
generateCryptoKeyFallback
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
import OpenPGP from "react-native-fast-openpgp";
|
||||
|
||||
export class KV {
|
||||
storage: MMKVInstance;
|
||||
@@ -136,11 +137,30 @@ export const Storage: IStorage = {
|
||||
clear(): Promise<void> {
|
||||
return DefaultStorage.clear();
|
||||
},
|
||||
generateCryptoKeyPair() {
|
||||
throw new Error("Not implemented");
|
||||
async generatePGPKeyPair() {
|
||||
const keys = await OpenPGP.generate({
|
||||
name: "NN",
|
||||
email: "NN@NN.NN"
|
||||
});
|
||||
return { publicKey: keys.publicKey, privateKey: keys.privateKey };
|
||||
},
|
||||
decryptAsymmetric() {
|
||||
throw new Error("Not implemented");
|
||||
async validatePGPKeyPair(keys) {
|
||||
try {
|
||||
const dummyData = JSON.stringify({
|
||||
favorite: true,
|
||||
title: "Hello world"
|
||||
});
|
||||
const encrypted = await OpenPGP.encrypt(dummyData, keys.publicKey);
|
||||
const decrypted = await OpenPGP.decrypt(encrypted, keys.privateKey, "");
|
||||
|
||||
return decrypted === dummyData;
|
||||
} catch (e) {
|
||||
console.error("PGP key pair validation error:", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async decryptPGPMessage(privateKeyArmored, encryptedMessage) {
|
||||
return await OpenPGP.decrypt(encryptedMessage, privateKeyArmored, "");
|
||||
},
|
||||
getAllKeys(): Promise<string[]> {
|
||||
return DefaultStorage.getAllKeys();
|
||||
|
||||
@@ -79,6 +79,7 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
|
||||
"rename-color",
|
||||
"rename-tag",
|
||||
"launcher-shortcut",
|
||||
"copy-link",
|
||||
"restore",
|
||||
"trash",
|
||||
"delete"
|
||||
|
||||
169
apps/mobile/app/components/sheets/add-api-key/index.tsx
Normal file
169
apps/mobile/app/components/sheets/add-api-key/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
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, { useRef, useState } from "react";
|
||||
import { View, ScrollView } from "react-native";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { db } from "../../../common/database";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { ToastManager, presentSheet } from "../../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
|
||||
const getExpiryOptions = () => [
|
||||
{ label: strings.expiryOneDay(), value: 24 * 60 * 60 * 1000 },
|
||||
{ label: strings.expiryOneWeek(), value: 7 * 24 * 60 * 60 * 1000 },
|
||||
{ label: strings.expiryOneMonth(), value: 30 * 24 * 60 * 60 * 1000 },
|
||||
{ label: strings.expiryOneYear(), value: 365 * 24 * 60 * 60 * 1000 },
|
||||
{ label: strings.never(), value: -1 }
|
||||
];
|
||||
|
||||
type AddApiKeySheetProps = {
|
||||
close?: (ctx?: string | undefined) => void;
|
||||
onAdd: () => void;
|
||||
};
|
||||
|
||||
export default function AddApiKeySheet({ close, onAdd }: AddApiKeySheetProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const keyNameRef = useRef<string>("");
|
||||
const [selectedExpiry, setSelectedExpiry] = useState(
|
||||
getExpiryOptions()[2].value
|
||||
);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
if (!keyNameRef.current || !keyNameRef.current.trim()) {
|
||||
ToastManager.show({
|
||||
message: strings.enterKeyName(),
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
await db.inboxApiKeys.create(keyNameRef.current, selectedExpiry);
|
||||
ToastManager.show({
|
||||
message: strings.apiKeyCreatedSuccessfully(),
|
||||
type: "success"
|
||||
});
|
||||
onAdd();
|
||||
close?.();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
ToastManager.show({
|
||||
message: strings.failedToCreateApiKey(message),
|
||||
type: "error"
|
||||
});
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingTop: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingBottom: DefaultAppStyles.GAP_VERTICAL * 2
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.xl}>{strings.createApiKey()}</Heading>
|
||||
</View>
|
||||
|
||||
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.keyName()}</Paragraph>
|
||||
<Input
|
||||
placeholder={strings.exampleKeyName()}
|
||||
onChangeText={(text) => {
|
||||
keyNameRef.current = text;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={{ gap: DefaultAppStyles.GAP_VERTICAL }}>
|
||||
<Paragraph size={AppFontSize.sm}>{strings.expiresIn()}</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
{getExpiryOptions().map((option) => (
|
||||
<Pressable
|
||||
key={option.label}
|
||||
onPress={() => setSelectedExpiry(option.value)}
|
||||
type={
|
||||
selectedExpiry === option.value ? "selected" : "transparent"
|
||||
}
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={AppFontSize.sm}
|
||||
color={
|
||||
selectedExpiry === option.value
|
||||
? colors.selected.paragraph
|
||||
: colors.primary.paragraph
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</Paragraph>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title={isCreating ? strings.creating() : strings.create()}
|
||||
type="accent"
|
||||
width="100%"
|
||||
loading={isCreating}
|
||||
disabled={isCreating}
|
||||
onPress={handleCreate}
|
||||
style={{
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
AddApiKeySheet.present = (onAdd: () => void) => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => (
|
||||
<AddApiKeySheet close={close} onAdd={onAdd} />
|
||||
)
|
||||
});
|
||||
};
|
||||
@@ -33,11 +33,11 @@ import { editorController } from "../../../screens/editor/tiptap/utils";
|
||||
import { eSendEvent, presentSheet } from "../../../services/event-manager";
|
||||
import { eUnlockNote } from "../../../utils/events";
|
||||
import { AppFontSize } from "../../../utils/size";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../../utils/styles";
|
||||
|
||||
const TabItemComponent = (props: {
|
||||
tab: TabItem;
|
||||
@@ -225,14 +225,24 @@ export default function EditorTabs({
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.lg}>{strings.tabs()}</Heading>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
useTabStore.getState().newTab();
|
||||
close?.();
|
||||
}}
|
||||
name="plus"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
<View style={{ flexDirection: "row", gap: DefaultAppStyles.GAP_SMALL }}>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
useTabStore.getState().clearAllTabs();
|
||||
close?.();
|
||||
}}
|
||||
name="close-box-multiple-outline"
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
useTabStore.getState().newTab();
|
||||
close?.();
|
||||
}}
|
||||
name="plus"
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
@@ -253,4 +263,4 @@ EditorTabs.present = () => {
|
||||
presentSheet({
|
||||
component: (ref, close, update) => <EditorTabs close={close} />
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -48,7 +48,6 @@ export const Notice = ({
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingLeft: 5,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.secondary.background,
|
||||
@@ -63,6 +62,9 @@ export const Notice = ({
|
||||
size={isSmall ? AppFontSize.md + 2 : AppFontSize.xxl}
|
||||
name={type}
|
||||
color={type === "alert" ? colors.error.icon : colors.primary.accent}
|
||||
style={{
|
||||
marginTop: isSmall ? 3 : 5
|
||||
}}
|
||||
/>
|
||||
<Paragraph
|
||||
style={{
|
||||
|
||||
@@ -1011,20 +1011,6 @@ export const useActions = ({
|
||||
icon: "history",
|
||||
onPress: openHistory
|
||||
},
|
||||
{
|
||||
id: "copy-link",
|
||||
title: strings.copyLink(),
|
||||
icon: "link",
|
||||
onPress: () => {
|
||||
Clipboard.setString(createInternalLink("note", item.id));
|
||||
ToastManager.show({
|
||||
heading: strings.linkCopied(),
|
||||
message: createInternalLink("note", item.id),
|
||||
context: "local",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "reminders",
|
||||
title: strings.dataTypesPluralCamelCase.reminder(),
|
||||
@@ -1280,5 +1266,28 @@ export const useActions = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
item.type === "tag" ||
|
||||
item.type === "note" ||
|
||||
item.type === "notebook" ||
|
||||
item.type === "color"
|
||||
) {
|
||||
actions.push({
|
||||
id: "copy-link",
|
||||
title: strings.copyLink(),
|
||||
icon: "link",
|
||||
onPress: () => {
|
||||
const link = createInternalLink(item.type, item.id);
|
||||
Clipboard.setString(link);
|
||||
ToastManager.show({
|
||||
heading: strings.linkCopied(),
|
||||
message: link,
|
||||
context: "local",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,9 @@ import {
|
||||
SYNC_CHECK_IDS,
|
||||
SubscriptionPlan,
|
||||
SyncStatusEvent,
|
||||
User
|
||||
User,
|
||||
isInternalLink,
|
||||
parseInternalLink
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import notifee from "@notifee/react-native";
|
||||
@@ -169,6 +171,8 @@ const onAppOpenedFromURL = async (event: {
|
||||
}) => {
|
||||
const url = event.url;
|
||||
|
||||
const parsedLink = isInternalLink(url) ? parseInternalLink(url) : undefined;
|
||||
|
||||
try {
|
||||
if (url.startsWith("https://app.notesnook.com/account/verified")) {
|
||||
await onUserEmailVerified();
|
||||
@@ -178,8 +182,12 @@ const onAppOpenedFromURL = async (event: {
|
||||
eSendEvent(eOnLoadNote, { newNote: true });
|
||||
fluidTabsRef.current?.goToPage("editor", false);
|
||||
return;
|
||||
} else if (url.startsWith("https://app.notesnook.com/open_note?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
} else if (
|
||||
parsedLink?.type === "note" ||
|
||||
url.startsWith("https://app.notesnook.com/open_note?")
|
||||
) {
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
|
||||
if (id) {
|
||||
const note = await db.notes.note(id);
|
||||
if (note) {
|
||||
@@ -191,13 +199,15 @@ const onAppOpenedFromURL = async (event: {
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
url.startsWith("https://app.notesnook.com/open_notebook?") &&
|
||||
(parsedLink?.type === "notebook" ||
|
||||
url.startsWith("https://app.notesnook.com/open_notebook?")) &&
|
||||
!event.isInitialUrl
|
||||
) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const notebook = await db.notebooks.notebook(id);
|
||||
if (notebook) {
|
||||
fluidTabsRef.current?.goToPage("home");
|
||||
Navigation.navigate("Notebook", {
|
||||
id: notebook.id,
|
||||
canGoBack: true,
|
||||
@@ -206,13 +216,15 @@ const onAppOpenedFromURL = async (event: {
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
url.startsWith("https://app.notesnook.com/open_tag?") &&
|
||||
(parsedLink?.type === "tag" ||
|
||||
url.startsWith("https://app.notesnook.com/open_tag?")) &&
|
||||
!event.isInitialUrl
|
||||
) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const tag = await db.tags.tag(id);
|
||||
if (tag) {
|
||||
fluidTabsRef.current?.goToPage("home");
|
||||
Navigation.navigate("TaggedNotes", {
|
||||
type: "tag",
|
||||
id: tag.id,
|
||||
@@ -222,13 +234,15 @@ const onAppOpenedFromURL = async (event: {
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
url.startsWith("https://app.notesnook.com/open_color?") &&
|
||||
(parsedLink?.type === "color" ||
|
||||
url.startsWith("https://app.notesnook.com/open_color?")) &&
|
||||
!event.isInitialUrl
|
||||
) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
const color = await db.colors.color(id);
|
||||
if (color) {
|
||||
fluidTabsRef.current?.goToPage("home");
|
||||
Navigation.navigate("ColoredNotes", {
|
||||
type: "color",
|
||||
id: color.id,
|
||||
|
||||
@@ -31,6 +31,7 @@ import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { rootNavigatorRef } from "../utils/global-refs";
|
||||
import Navigation from "../services/navigation";
|
||||
import { isFeatureAvailable } from "@notesnook/common";
|
||||
import { isInternalLink, parseInternalLink } from "@notesnook/core";
|
||||
|
||||
const RootStack = createNativeStackNavigator();
|
||||
const AppStack = createNativeStackNavigator();
|
||||
@@ -63,10 +64,17 @@ const AppNavigation = React.memo(
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!home) {
|
||||
if (useSettingStore.getState().initialUrl) {
|
||||
const url = useSettingStore.getState().initialUrl;
|
||||
if (url?.startsWith("https://app.notesnook.com/open_notebook?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
const url = useSettingStore.getState().initialUrl;
|
||||
if (url) {
|
||||
const parsedLink = isInternalLink(url)
|
||||
? parseInternalLink(url)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
parsedLink?.type === "notebook" ||
|
||||
url?.startsWith("https://app.notesnook.com/open_notebook?")
|
||||
) {
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
setHome({
|
||||
name: "Notebook",
|
||||
@@ -76,8 +84,11 @@ const AppNavigation = React.memo(
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (url?.startsWith("https://app.notesnook.com/open_tag?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
} else if (
|
||||
parsedLink?.type === "tag" ||
|
||||
url?.startsWith("https://app.notesnook.com/open_tag?")
|
||||
) {
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
setHome({
|
||||
name: "TaggedNotes",
|
||||
@@ -88,8 +99,11 @@ const AppNavigation = React.memo(
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (url?.startsWith("https://app.notesnook.com/open_color?")) {
|
||||
const id = new URL(url).searchParams.get("id");
|
||||
} else if (
|
||||
parsedLink?.type === "color" ||
|
||||
url?.startsWith("https://app.notesnook.com/open_color?")
|
||||
) {
|
||||
const id = parsedLink?.id || new URL(url).searchParams.get("id");
|
||||
if (id) {
|
||||
setHome({
|
||||
name: "ColoredNotes",
|
||||
|
||||
@@ -187,6 +187,7 @@ export type TabStore = {
|
||||
focusEmptyTab: () => void;
|
||||
getCurrentNoteId: () => string | undefined;
|
||||
getTab: (tabId: string) => TabItem | undefined;
|
||||
clearAllTabs: () => void;
|
||||
newTabSession: (
|
||||
id: string,
|
||||
options?: Omit<Partial<TabSessionItem>, "id">
|
||||
@@ -255,7 +256,7 @@ export const useTabStore = create<TabStore, any>(
|
||||
|
||||
const sessionId =
|
||||
oldSessionId &&
|
||||
tabSessionHistory.currentSessionId(tabId) === oldSessionId
|
||||
tabSessionHistory.currentSessionId(tabId) === oldSessionId
|
||||
? oldSessionId
|
||||
: tabSessionHistory.add(tabId, oldSessionId);
|
||||
|
||||
@@ -386,7 +387,7 @@ export const useTabStore = create<TabStore, any>(
|
||||
focusPreviewTab: (
|
||||
noteId: string,
|
||||
options: Omit<Partial<TabItem>, "id" | "noteId">
|
||||
) => {},
|
||||
) => { },
|
||||
|
||||
removeTab: (id: string) => {
|
||||
const index = get().tabs.findIndex((t) => t.id === id);
|
||||
@@ -482,6 +483,25 @@ export const useTabStore = create<TabStore, any>(
|
||||
},
|
||||
getTab: (tabId) => {
|
||||
return get().tabs.find((t) => t.id === tabId);
|
||||
},
|
||||
clearAllTabs: () => {
|
||||
const tabs = get().tabs;
|
||||
tabs.forEach((tab) => {
|
||||
const tabSessions = tabSessionHistory.getTabHistory(tab.id);
|
||||
tabSessions.back.forEach((id) => TabSessionStorage.remove(id));
|
||||
tabSessions.forward.forEach((id) => TabSessionStorage.remove(id));
|
||||
tabSessionHistory.clearStackForTab(tab.id);
|
||||
});
|
||||
|
||||
const id = getId();
|
||||
set({
|
||||
tabs: [{ id: id }],
|
||||
currentTab: id
|
||||
});
|
||||
history.history = [id];
|
||||
get().newTabSession(id);
|
||||
get().focusTab(id);
|
||||
syncTabs();
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ import SoundPicker from "./sound-picker";
|
||||
import ThemeSelector from "./theme-selector";
|
||||
import { TitleFormat } from "./title-format";
|
||||
import { NotesnookCircle } from "./notesnook-circle";
|
||||
import { ManageInboxKeys, InboxKeysList } from "./manage-inbox-keys";
|
||||
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
homeselector: <HomePicker />,
|
||||
@@ -75,5 +76,7 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"sidebar-tab-selector": <SidebarTabPicker />,
|
||||
"change-password": <ChangePassword />,
|
||||
"change-email": <ChangeEmail />,
|
||||
"notesnook-circle": <NotesnookCircle />
|
||||
"notesnook-circle": <NotesnookCircle />,
|
||||
"manage-inbox-keys": <ManageInboxKeys />,
|
||||
"inbox-keys": <InboxKeysList />
|
||||
};
|
||||
|
||||
474
apps/mobile/app/screens/settings/manage-inbox-keys.tsx
Normal file
474
apps/mobile/app/screens/settings/manage-inbox-keys.tsx
Normal file
@@ -0,0 +1,474 @@
|
||||
import { usePromise } from "@notesnook/common";
|
||||
import { db } from "../../common/database";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Input from "../../components/ui/input";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { SerializedKeyPair } from "@notesnook/crypto";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { DefaultAppStyles } from "../../utils/styles";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { Storage } from "../../common/database/storage";
|
||||
import { Notice } from "../../components/ui/notice";
|
||||
import { presentDialog } from "../../components/dialog/functions";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { InboxApiKey } from "@notesnook/core";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import dayjs from "dayjs";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import { AppFontSize } from "../../utils/size";
|
||||
import AddApiKeySheet from "../../components/sheets/add-api-key";
|
||||
|
||||
const ManageInboxKeys = () => {
|
||||
const keys = usePromise(() => db.user.getInboxKeys());
|
||||
const keysEdited = useRef<SerializedKeyPair>(undefined);
|
||||
|
||||
if (keys.status === "fulfilled" && keys.value && !keysEdited.current) {
|
||||
keysEdited.current = keys.value;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
marginTop: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<Notice type="alert" text={strings.changingInboxPgpKeysNotice()} />
|
||||
|
||||
<Paragraph>{strings.publicKey()}</Paragraph>
|
||||
<Input
|
||||
defaultValue={keysEdited.current?.publicKey}
|
||||
multiline
|
||||
onChangeText={(value) => {
|
||||
if (keysEdited.current) {
|
||||
keysEdited.current.publicKey = value;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
height: 150
|
||||
}}
|
||||
wrapperStyle={{
|
||||
height: 150
|
||||
}}
|
||||
/>
|
||||
<Paragraph>{strings.privateKey()}</Paragraph>
|
||||
<Input
|
||||
defaultValue={keysEdited.current?.privateKey}
|
||||
multiline
|
||||
onChangeText={(value) => {
|
||||
if (keysEdited.current) {
|
||||
keysEdited.current.privateKey = value;
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
height: 150
|
||||
}}
|
||||
wrapperStyle={{
|
||||
height: 150
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={strings.save()}
|
||||
type="accent"
|
||||
width={"100%"}
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (keysEdited.current) {
|
||||
const valid = await Storage.validatePGPKeyPair(
|
||||
keysEdited.current
|
||||
);
|
||||
|
||||
if (!valid) {
|
||||
ToastManager.show({
|
||||
message: strings.invalidPgpKeyPair(),
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
presentDialog({
|
||||
title: strings.areYouSure(),
|
||||
paragraph: strings.changingInboxPgpKeysNotice(),
|
||||
positiveText: strings.yes(),
|
||||
negativeText: strings.no(),
|
||||
positivePress: async () => {
|
||||
db.user?.saveInboxKeys(keysEdited.current!);
|
||||
ToastManager.show({
|
||||
message: strings.inboxKeysSaved(),
|
||||
type: "success"
|
||||
});
|
||||
Navigation.goBack();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
ToastManager.error(e as Error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const InboxKeysList = () => {
|
||||
const inboxEnabled = useSettingStore((state) => state.inboxEnabled);
|
||||
const apiKeysPromise = usePromise(
|
||||
() => db.inboxApiKeys.get(),
|
||||
[inboxEnabled]
|
||||
);
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
if (apiKeysPromise.status === "pending") {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="small" color={colors.primary.accent} />
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.loadingApiKeys()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (apiKeysPromise.status === "rejected") {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.error.paragraph}>
|
||||
{strings.failedToLoadApiKeys()}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={strings.retry()}
|
||||
type="accent"
|
||||
onPress={() => apiKeysPromise.refresh()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = apiKeysPromise.value || [];
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: DefaultAppStyles.GAP_VERTICAL,
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
width: "100%",
|
||||
paddingHorizontal: DefaultAppStyles.GAP,
|
||||
paddingBottom: 50
|
||||
}}
|
||||
>
|
||||
{apiKeys.length === 0 ? (
|
||||
<View
|
||||
style={{
|
||||
padding: DefaultAppStyles.GAP * 2,
|
||||
borderWidth: 1,
|
||||
borderStyle: "dashed",
|
||||
borderColor: colors.secondary.border,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{strings.createFirstApiKey()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ gap: 0 }}>
|
||||
{apiKeys.map((key, i) => (
|
||||
<ApiKeyItem
|
||||
key={key.key}
|
||||
apiKey={key}
|
||||
onRevoke={() => apiKeysPromise.refresh()}
|
||||
isAtEnd={i === apiKeys.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
title={strings.createKey()}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
|
||||
width: "100%"
|
||||
}}
|
||||
onPress={() => {
|
||||
if (apiKeys.length >= 10) {
|
||||
presentDialog({
|
||||
title: strings.apiKeysLimitReached(),
|
||||
paragraph: strings.apiKeysLimitReachedMessage(),
|
||||
positiveText: strings.ok()
|
||||
});
|
||||
} else {
|
||||
AddApiKeySheet.present(() => apiKeysPromise.refresh());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const VIEW_KEY_TIMEOUT = 15;
|
||||
|
||||
type ApiKeyItemProps = {
|
||||
apiKey: InboxApiKey;
|
||||
onRevoke: () => void;
|
||||
isAtEnd: boolean;
|
||||
};
|
||||
|
||||
function ApiKeyItem({ apiKey, onRevoke, isAtEnd }: ApiKeyItemProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [viewing, setViewing] = useState(false);
|
||||
const [isRevoking, setIsRevoking] = useState(false);
|
||||
const [secondsLeft, setSecondsLeft] = useState(VIEW_KEY_TIMEOUT);
|
||||
|
||||
async function viewKey() {
|
||||
presentDialog({
|
||||
title: strings.authenticateToViewApiKey(),
|
||||
paragraph: strings.enterPasswordToViewApiKey(),
|
||||
positiveText: strings.authenticate(),
|
||||
negativeText: strings.cancel(),
|
||||
input: true,
|
||||
secureTextEntry: true,
|
||||
inputPlaceholder: strings.accountPassword(),
|
||||
positivePress: async (value) => {
|
||||
try {
|
||||
const verified = await db.user.verifyPassword(value);
|
||||
if (!verified) {
|
||||
ToastManager.show({
|
||||
message: strings.invalidPassword(),
|
||||
type: "error"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
setViewing(true);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ToastManager.error(error as Error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function copyToClipboard() {
|
||||
if (!viewing) return;
|
||||
try {
|
||||
Clipboard.setString(apiKey.key);
|
||||
setCopied(true);
|
||||
ToastManager.show({
|
||||
message: strings.apiKeyCopiedToClipboard(),
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
ToastManager.show({
|
||||
message: strings.failedToCopyToClipboard(),
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
const timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [copied]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewing) {
|
||||
setSecondsLeft(VIEW_KEY_TIMEOUT);
|
||||
const interval = setInterval(() => {
|
||||
setSecondsLeft((prev) => {
|
||||
if (prev <= 1) {
|
||||
setViewing(false);
|
||||
return VIEW_KEY_TIMEOUT;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [viewing]);
|
||||
|
||||
const isApiKeyExpired = Date.now() > apiKey.expiryDate;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
|
||||
borderBottomWidth: isAtEnd ? 0 : 1,
|
||||
borderBottomColor: colors.secondary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
gap: DefaultAppStyles.GAP_VERTICAL
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<Heading size={AppFontSize.md}>{apiKey.name}</Heading>
|
||||
{isApiKeyExpired && (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 8,
|
||||
backgroundColor: colors.error.background,
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
color={colors.static.white}
|
||||
size={AppFontSize.xxs}
|
||||
style={{ fontWeight: "bold" }}
|
||||
>
|
||||
EXPIRED
|
||||
</Paragraph>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={{ gap: 4 }}>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{apiKey.lastUsedAt
|
||||
? `${strings.lastUsedOn()} ${dayjs(apiKey.lastUsedAt).format("MMM DD, YYYY")}`
|
||||
: strings.neverUsed()}
|
||||
</Paragraph>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{strings.createdOn()}{" "}
|
||||
{dayjs(apiKey.dateCreated).format("MMM DD, YYYY")}
|
||||
</Paragraph>
|
||||
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
|
||||
{apiKey.expiryDate === -1
|
||||
? strings.neverExpires()
|
||||
: `${isApiKeyExpired ? strings.expired() : strings.expiresOn()} ${dayjs(apiKey.expiryDate).format("MMM DD, YYYY")}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: DefaultAppStyles.GAP_SMALL
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
editable={false}
|
||||
value={
|
||||
viewing
|
||||
? apiKey.key
|
||||
: `${apiKey.key.slice(0, 10)}${"*".repeat(
|
||||
apiKey.key.length - 10
|
||||
)}`
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontFamily: "monospace",
|
||||
fontSize: AppFontSize.xs
|
||||
}}
|
||||
wrapperStyle={{
|
||||
flex: 1
|
||||
}}
|
||||
/>
|
||||
{!viewing && (
|
||||
<IconButton
|
||||
name="eye-off-outline"
|
||||
color={colors.primary.icon}
|
||||
onPress={() => viewKey()}
|
||||
/>
|
||||
)}
|
||||
{viewing && (
|
||||
<>
|
||||
<Paragraph
|
||||
style={{
|
||||
fontFamily: "monospace",
|
||||
minWidth: 35,
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{secondsLeft}s
|
||||
</Paragraph>
|
||||
<IconButton
|
||||
name={copied ? "check" : "content-copy"}
|
||||
color={colors.primary.icon}
|
||||
onPress={() => copyToClipboard()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
name="delete-outline"
|
||||
color={colors.error.icon}
|
||||
disabled={isRevoking}
|
||||
onPress={async () => {
|
||||
presentDialog({
|
||||
title: strings.revokeInboxApiKey(apiKey.name),
|
||||
paragraph: strings.revokeApiKeyConfirmation(apiKey.name),
|
||||
positiveText: strings.revoke(),
|
||||
negativeText: strings.cancel(),
|
||||
positiveType: "error",
|
||||
positivePress: async () => {
|
||||
try {
|
||||
setIsRevoking(true);
|
||||
await db.inboxApiKeys.revoke(apiKey.key);
|
||||
onRevoke();
|
||||
ToastManager.show({
|
||||
message: strings.apiKeyRevoked(),
|
||||
type: "success"
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
ToastManager.show({
|
||||
message: strings.failedToRevokeApiKey(),
|
||||
type: "error"
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
setIsRevoking(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export { ManageInboxKeys, InboxKeysList };
|
||||
@@ -233,19 +233,22 @@ const _SectionItem = ({ item }: { item: SettingSection }) => {
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
paddingRight: item.type === "switch" ? 10 : 0
|
||||
paddingRight: item.type === "switch" ? 10 : 0,
|
||||
flex: item.type === "component" ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.heading
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{typeof item.name === "function" ? item.name(current) : item.name}
|
||||
</Heading>
|
||||
{item.name ? (
|
||||
<Heading
|
||||
color={
|
||||
item.type === "danger"
|
||||
? colors.error.paragraph
|
||||
: colors.primary.heading
|
||||
}
|
||||
size={AppFontSize.sm}
|
||||
>
|
||||
{typeof item.name === "function" ? item.name(current) : item.name}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{!!item.description && (
|
||||
<Paragraph
|
||||
|
||||
@@ -81,6 +81,7 @@ import { MMKV } from "../../common/database/mmkv";
|
||||
import { resetTabStore } from "../editor/tiptap/use-tab-store";
|
||||
import { clearAllStores } from "../../stores";
|
||||
import { refreshAllStores } from "../../stores/create-db-collection-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
export const settingsGroups: SettingSection[] = [
|
||||
{
|
||||
@@ -686,6 +687,63 @@ export const settingsGroups: SettingSection[] = [
|
||||
type: "screen",
|
||||
description: strings.notesnookCircleDesc(),
|
||||
component: "notesnook-circle"
|
||||
},
|
||||
{
|
||||
id: "inbox-api",
|
||||
name: strings.inboxAPI(),
|
||||
icon: "inbox",
|
||||
type: "screen",
|
||||
description: strings.inboxAPIDesc(),
|
||||
sections: [
|
||||
{
|
||||
id: "toggle-inbox-api",
|
||||
name: strings.enableInboxAPI(),
|
||||
description: strings.enableInboxAPIDesc(),
|
||||
type: "switch",
|
||||
useHook: () => useSettingStore((state) => state.inboxEnabled),
|
||||
getter: (current) => current,
|
||||
modifer: async (current) => {
|
||||
if (current) {
|
||||
presentDialog({
|
||||
title: strings.disableInboxAPI(),
|
||||
paragraph: strings.disableInboxAPIDesc(),
|
||||
positiveText: strings.disable(),
|
||||
positivePress: async () => {
|
||||
await db.user.discardInboxKeys();
|
||||
useSettingStore.setState({
|
||||
inboxEnabled: false
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await db.user.getInboxKeys();
|
||||
useSettingStore.setState({
|
||||
inboxEnabled: true
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "manage-inbox-keys",
|
||||
name: strings.manageInboxKeys(),
|
||||
description: strings.manageInboxKeysDesc(),
|
||||
type: "screen",
|
||||
component: "manage-inbox-keys"
|
||||
},
|
||||
{
|
||||
id: "inbox-keys",
|
||||
name: strings.viewAPIKeys(),
|
||||
description: strings.viewAPIKeysDesc(),
|
||||
type: "screen",
|
||||
component: "inbox-keys"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -143,6 +143,7 @@ export interface SettingStore {
|
||||
isOldAppLock: () => boolean;
|
||||
initialUrl: string | null;
|
||||
refresh: () => void;
|
||||
inboxEnabled: boolean;
|
||||
}
|
||||
|
||||
const { width, height } = Dimensions.get("window");
|
||||
@@ -248,12 +249,14 @@ export const useSettingStore = create<SettingStore>((set, get) => ({
|
||||
? initialWindowMetrics.insets
|
||||
: { top: 0, right: 0, left: 0, bottom: 0 },
|
||||
initialUrl: null,
|
||||
refresh: () => {
|
||||
refresh: async () => {
|
||||
set({
|
||||
dayFormat: db.settings.getDayFormat(),
|
||||
timeFormat: db.settings.getTimeFormat(),
|
||||
dateFormat: db.settings?.getTimeFormat(),
|
||||
weekFormat: db.settings.getWeekFormat()
|
||||
weekFormat: db.settings.getWeekFormat(),
|
||||
inboxEnabled: await db.user.hasInboxKeys()
|
||||
});
|
||||
}
|
||||
},
|
||||
inboxEnabled: false
|
||||
}));
|
||||
|
||||
Binary file not shown.
@@ -9,6 +9,9 @@ import "./app/common/logger/index";
|
||||
import { setI18nGlobal } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
import Config from "react-native-config";
|
||||
import OpenPGP from "react-native-fast-openpgp";
|
||||
|
||||
OpenPGP.useJSI = false;
|
||||
|
||||
let domParser;
|
||||
Object.defineProperty(global, "DOMParser", {
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia</string>
|
||||
<string>nn</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
|
||||
27
apps/mobile/package-lock.json
generated
27
apps/mobile/package-lock.json
generated
@@ -84,6 +84,7 @@
|
||||
"react-native-device-info": "^14.1.1",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-exit-app": "github:ammarahm-ed/react-native-exit-app",
|
||||
"react-native-fast-openpgp": "^2.9.3",
|
||||
"react-native-file-viewer": "^2.1.1",
|
||||
"react-native-format-currency": "0.0.5",
|
||||
"react-native-gesture-handler": "2.28.0",
|
||||
@@ -10882,6 +10883,12 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flatbuffers": {
|
||||
"version": "24.3.25",
|
||||
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.3.25.tgz",
|
||||
"integrity": "sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
@@ -17531,6 +17538,26 @@
|
||||
"resolved": "git+ssh://git@github.com/ammarahm-ed/react-native-exit-app.git#3087d4bce1320227384d24b34b354600457a817d",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-fast-openpgp": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-fast-openpgp/-/react-native-fast-openpgp-2.9.3.tgz",
|
||||
"integrity": "sha512-JF+h0e45mi1kHSYfpXIDWnH5x3uFHU8DqGsuqZo7JWKfmE6MF7ppwvI4dyc5rUdGoZI752IdxI33F86UcR/fnQ==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"dependencies": {
|
||||
"big-integer": "^1.6.51",
|
||||
"flatbuffers": "24.3.25"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-file-viewer": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/react-native-file-viewer/-/react-native-file-viewer-2.1.5.tgz",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"react-native-device-info": "^14.1.1",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-exit-app": "github:ammarahm-ed/react-native-exit-app",
|
||||
"react-native-fast-openpgp": "^2.9.3",
|
||||
"react-native-file-viewer": "^2.1.1",
|
||||
"react-native-format-currency": "0.0.5",
|
||||
"react-native-gesture-handler": "2.28.0",
|
||||
|
||||
@@ -61,6 +61,7 @@ const EXTRA_ICON_NAMES = [
|
||||
"checkbox-marked",
|
||||
"checkbox-blank-outline",
|
||||
"unfold-less-horizontal",
|
||||
"close-box-multiple-outline",
|
||||
"minus-circle",
|
||||
"vibrate",
|
||||
"volume-high",
|
||||
@@ -121,7 +122,8 @@ const EXTRA_ICON_NAMES = [
|
||||
"calendar-today",
|
||||
"bomb",
|
||||
"bomb-off",
|
||||
"cancel"
|
||||
"cancel",
|
||||
"inbox"
|
||||
];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
1422
apps/web/package-lock.json
generated
1422
apps/web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -114,6 +114,7 @@
|
||||
"happy-dom": "16.0.1",
|
||||
"ip": "^2.0.1",
|
||||
"lorem-ipsum": "^2.0.4",
|
||||
"openpgp": "^6.2.2",
|
||||
"otplib": "^13.3.0",
|
||||
"rollup-plugin-visualizer": "^5.13.1",
|
||||
"vite": "5.4.11",
|
||||
|
||||
@@ -25,16 +25,17 @@ import { useStore as useAnnouncementStore } from "./stores/announcement-store";
|
||||
import { useStore as useSettingStore } from "./stores/setting-store";
|
||||
import { scheduleBackups, scheduleFullBackups } from "./common/notices";
|
||||
import {
|
||||
handleInternalLink,
|
||||
introduceFeatures,
|
||||
resetFeatures,
|
||||
scheduleExpiredNotesDeletion
|
||||
} from "./common";
|
||||
import { AppEventManager, AppEvents } from "./common/app-events";
|
||||
import { db } from "./common/db";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { EVENTS, parseInternalLink } from "@notesnook/core";
|
||||
import { registerKeyMap } from "./common/key-map";
|
||||
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
|
||||
import { hashNavigate } from "./navigation";
|
||||
import { hashNavigate, navigate } from "./navigation";
|
||||
import { desktop } from "./common/desktop-bridge";
|
||||
import { FeatureDialog } from "./dialogs/feature-dialog";
|
||||
import { AnnouncementDialog } from "./dialogs/announcement-dialog";
|
||||
@@ -56,9 +57,10 @@ export default function AppEffects() {
|
||||
useEffect(
|
||||
function initializeApp() {
|
||||
initStore();
|
||||
initEditorStore();
|
||||
|
||||
(async function () {
|
||||
await initEditorStore();
|
||||
await attachDesktopListeners();
|
||||
await resetFeatures();
|
||||
await refreshNavItems();
|
||||
await updateLastSynced();
|
||||
@@ -201,29 +203,6 @@ export default function AppEffects() {
|
||||
})();
|
||||
}, [dialogAnnouncements]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } =
|
||||
desktop?.bridge.onCreateItem.subscribe(undefined, {
|
||||
onData(itemType) {
|
||||
switch (itemType) {
|
||||
case "note":
|
||||
useEditorStore.getState().newSession();
|
||||
break;
|
||||
case "notebook":
|
||||
hashNavigate("/notebooks/create", { replace: true });
|
||||
break;
|
||||
case "reminder":
|
||||
hashNavigate("/reminders/create", { replace: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}) || {};
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <React.Fragment />;
|
||||
}
|
||||
|
||||
@@ -240,3 +219,27 @@ function getProcessingStatusFromType(type: ProcessingType) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function attachDesktopListeners() {
|
||||
const handlers = [
|
||||
AppEventManager.subscribe(AppEvents.onCreateItem, onCreateItem),
|
||||
AppEventManager.subscribe(AppEvents.onOpenLink, handleInternalLink)
|
||||
];
|
||||
|
||||
await desktop?.bridge.ready.query();
|
||||
return handlers;
|
||||
}
|
||||
|
||||
async function onCreateItem(itemType: string) {
|
||||
switch (itemType) {
|
||||
case "note":
|
||||
useEditorStore.getState().newSession();
|
||||
break;
|
||||
case "notebook":
|
||||
hashNavigate("/notebooks/create", { replace: true });
|
||||
break;
|
||||
case "reminder":
|
||||
hashNavigate("/reminders/create", { replace: true });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,5 +41,8 @@ export const AppEvents = {
|
||||
revealItemInList: "list:revealItem",
|
||||
|
||||
toggleSideMenu: "app:openSideMenu",
|
||||
toggleEditor: "app:toggleEditor"
|
||||
toggleEditor: "app:toggleEditor",
|
||||
|
||||
onOpenLink: "onOpenLink",
|
||||
onCreateItem: "onCreateItem"
|
||||
};
|
||||
|
||||
@@ -24,8 +24,6 @@ import { AppEventManager, AppEvents } from "../app-events";
|
||||
import { TaskScheduler } from "../../utils/task-scheduler";
|
||||
import { checkForUpdate } from "../../utils/updater";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { db } from "../db";
|
||||
import { logManager } from "@notesnook/core";
|
||||
import { store as settingStore } from "../../stores/setting-store";
|
||||
|
||||
export const desktop: ReturnType<typeof createTRPCProxyClient<AppRouter>> =
|
||||
@@ -67,6 +65,16 @@ function attachListeners() {
|
||||
attachListener(AppEvents.updateError)
|
||||
);
|
||||
|
||||
desktop.bridge.onOpenLink.subscribe(
|
||||
undefined,
|
||||
attachListener(AppEvents.onOpenLink)
|
||||
);
|
||||
|
||||
desktop.bridge.onCreateItem.subscribe(
|
||||
undefined,
|
||||
attachListener(AppEvents.onCreateItem)
|
||||
);
|
||||
|
||||
// desktop.window.onClose.subscribe(undefined, {
|
||||
// async onData() {
|
||||
// try {
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Config from "../utils/config";
|
||||
import { hashNavigate, getCurrentHash } from "../navigation";
|
||||
import { hashNavigate, getCurrentHash, navigate } from "../navigation";
|
||||
import { db } from "./db";
|
||||
import {
|
||||
areFeaturesAvailable,
|
||||
@@ -39,7 +39,7 @@ import { readFile, showFilePicker } from "../utils/file-picker";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PATHS } from "@notesnook/desktop";
|
||||
import { TaskManager } from "./task-manager";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { EVENTS, parseInternalLink } from "@notesnook/core";
|
||||
import { createWritableStream } from "./desktop-bridge";
|
||||
import { FeatureDialog, FeatureKeys } from "../dialogs/feature-dialog";
|
||||
import { User } from "@notesnook/core";
|
||||
@@ -569,3 +569,20 @@ export async function scheduleExpiredNotesDeletion() {
|
||||
await db.notes.deleteExpiredNotes();
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleInternalLink(url: string, openInNewTab?: boolean) {
|
||||
const link = parseInternalLink(url);
|
||||
if (!link) return;
|
||||
if (link.type === "note") {
|
||||
await useEditorStore.getState().openSession(link.id, {
|
||||
activeBlockId: link.params?.blockId || undefined,
|
||||
openInNewTab
|
||||
});
|
||||
} else if (link.type === "notebook") {
|
||||
navigate(`/notebooks/${link.id}`);
|
||||
} else if (link.type === "tag") {
|
||||
navigate(`/tags/${link.id}`);
|
||||
} else if (link.type === "color") {
|
||||
navigate(`/colors/${link.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import { showFeatureNotAllowedToast } from "../../common/toasts";
|
||||
import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog";
|
||||
import { ConfirmDialog } from "../../dialogs/confirm";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { handleInternalLink } from "../../common";
|
||||
|
||||
export type OnChangeHandler = (
|
||||
content: () => string,
|
||||
@@ -423,12 +424,8 @@ function TipTap(props: TipTapProps) {
|
||||
getAttachmentData: onGetAttachmentData,
|
||||
openLink: (url, openInNewTab) => {
|
||||
const link = parseInternalLink(url);
|
||||
if (link && link.type === "note") {
|
||||
useEditorStore.getState().openSession(link.id, {
|
||||
activeBlockId: link.params?.blockId || undefined,
|
||||
openInNewTab: openInNewTab
|
||||
});
|
||||
} else window.open(url, "_blank");
|
||||
if (link) handleInternalLink(url, openInNewTab);
|
||||
else window.open(url, "_blank");
|
||||
}
|
||||
};
|
||||
}, [
|
||||
|
||||
@@ -47,7 +47,8 @@ import {
|
||||
Notebook as NotebookIcon,
|
||||
Plus,
|
||||
SortBy,
|
||||
Tag as TagIcon
|
||||
Tag as TagIcon,
|
||||
InternalLink
|
||||
} from "../icons";
|
||||
import { SortableNavigationItem } from "./navigation-item";
|
||||
import {
|
||||
@@ -89,7 +90,7 @@ import {
|
||||
} from "@dnd-kit/sortable";
|
||||
import { usePersistentState } from "../../hooks/use-persistent-state";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Color, Notebook, Tag } from "@notesnook/core";
|
||||
import { Color, createInternalLink, Notebook, Tag } from "@notesnook/core";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { Menu, useMenuStore, useMenuTrigger } from "../../hooks/use-menu";
|
||||
import { RenameColorDialog } from "../../dialogs/item-dialog";
|
||||
@@ -117,6 +118,7 @@ import {
|
||||
} from "@notesnook/common";
|
||||
import { isUserSubscribed } from "../../hooks/use-is-user-premium";
|
||||
import { shouldShowWrapped } from "../../utils/should-show-wrapped";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
|
||||
type Route = {
|
||||
id: "notes" | "favorites" | "reminders" | "monographs" | "trash" | "archive";
|
||||
@@ -641,9 +643,23 @@ function ColorItem({
|
||||
},
|
||||
icon: Trash.path
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "copy-link",
|
||||
title: strings.copyLink(),
|
||||
icon: InternalLink.path,
|
||||
onClick: () => {
|
||||
const link = createInternalLink("color", color.id);
|
||||
writeToClipboard({
|
||||
"text/plain": link,
|
||||
"text/html": `<a href="${link}">${color.title}</a>`,
|
||||
"text/markdown": `[${color.title}](${link})`
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "lazy-loader",
|
||||
key: "sidebar-items-loader",
|
||||
key: "sidebar-items-loader2",
|
||||
items: async () => [
|
||||
createSetDefaultHomepageMenuItem(
|
||||
color.id,
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import ListItem from "../list-item";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { useStore as useNotesStore } from "../../stores/note-store";
|
||||
import { Notebook as NotebookType } from "@notesnook/core";
|
||||
import { createInternalLink, Notebook as NotebookType } from "@notesnook/core";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -31,7 +31,9 @@ import {
|
||||
Trash,
|
||||
Notebook as NotebookIcon,
|
||||
ArrowUp,
|
||||
Move
|
||||
Move,
|
||||
Copy,
|
||||
InternalLink
|
||||
} from "../icons";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { hashNavigate, navigate } from "../../navigation";
|
||||
@@ -51,6 +53,7 @@ import {
|
||||
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||
import { MoveNotebookDialog } from "../../dialogs/move-notebook-dialog";
|
||||
import { areFeaturesAvailable } from "@notesnook/common";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
|
||||
type NotebookProps = {
|
||||
item: NotebookType;
|
||||
@@ -254,6 +257,20 @@ export const notebookMenuItems: (
|
||||
MoveNotebookDialog.show({ notebook: notebook });
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "copy-link",
|
||||
title: strings.copyLink(),
|
||||
icon: InternalLink.path,
|
||||
onClick: () => {
|
||||
const link = createInternalLink("notebook", notebook.id);
|
||||
writeToClipboard({
|
||||
"text/plain": link,
|
||||
"text/html": `<a href="${link}">${notebook.title}</a>`,
|
||||
"text/markdown": `[${notebook.title}](${link})`
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "move-to-top",
|
||||
|
||||
@@ -22,9 +22,15 @@ import { navigate } from "../../navigation";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { store as appStore } from "../../stores/app-store";
|
||||
import { db } from "../../common/db";
|
||||
import { Edit, Shortcut, DeleteForver, Tag as TagIcon } from "../icons";
|
||||
import {
|
||||
Edit,
|
||||
Shortcut,
|
||||
DeleteForver,
|
||||
Tag as TagIcon,
|
||||
InternalLink
|
||||
} from "../icons";
|
||||
import { MenuItem } from "@notesnook/ui";
|
||||
import { Tag as TagType } from "@notesnook/core";
|
||||
import { createInternalLink, Tag as TagType } from "@notesnook/core";
|
||||
import { handleDrop } from "../../common/drop-handler";
|
||||
import { EditTagDialog } from "../../dialogs/item-dialog";
|
||||
import { useStore as useSelectionStore } from "../../stores/selection-store";
|
||||
@@ -36,6 +42,7 @@ import {
|
||||
withFeatureCheck
|
||||
} from "../../common";
|
||||
import { areFeaturesAvailable } from "@notesnook/common";
|
||||
import { writeToClipboard } from "../../utils/clipboard";
|
||||
|
||||
type TagProps = { item: TagType; totalNotes: number };
|
||||
function Tag(props: TagProps) {
|
||||
@@ -149,6 +156,20 @@ export const tagMenuItems: (
|
||||
appStore.addToShortcuts(tag)
|
||||
)
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "copy-link",
|
||||
title: strings.copyLink(),
|
||||
icon: InternalLink.path,
|
||||
onClick: () => {
|
||||
const link = createInternalLink("tag", tag.id);
|
||||
writeToClipboard({
|
||||
"text/plain": link,
|
||||
"text/html": `<a href="${link}">${tag.title}</a>`,
|
||||
"text/markdown": `[${tag.title}](${link})`
|
||||
});
|
||||
}
|
||||
},
|
||||
{ key: "sep", type: "separator" },
|
||||
{
|
||||
type: "button",
|
||||
|
||||
210
apps/web/src/dialogs/inbox-pgp-keys-dialog.tsx
Normal file
210
apps/web/src/dialogs/inbox-pgp-keys-dialog.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
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 { useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { db } from "../common/db";
|
||||
import Field from "../components/field";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { SerializedKeyPair } from "@notesnook/crypto";
|
||||
import { ConfirmDialog } from "./confirm";
|
||||
|
||||
type InboxPGPKeysDialogProps = BaseDialogProps<boolean> & {
|
||||
keys?: SerializedKeyPair | null;
|
||||
};
|
||||
|
||||
export const InboxPGPKeysDialog = DialogManager.register(
|
||||
function InboxPGPKeysDialog(props: InboxPGPKeysDialogProps) {
|
||||
const { keys: initialKeys, onClose } = props;
|
||||
const [mode, setMode] = useState<"choose" | "edit">(
|
||||
initialKeys ? "edit" : "choose"
|
||||
);
|
||||
const [publicKey, setPublicKey] = useState(initialKeys?.publicKey || "");
|
||||
const [privateKey, setPrivateKey] = useState(initialKeys?.privateKey || "");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const hasChanges =
|
||||
publicKey !== (initialKeys?.publicKey || "") ||
|
||||
privateKey !== (initialKeys?.privateKey || "");
|
||||
|
||||
async function handleAutoGenerate() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await db.user.getInboxKeys();
|
||||
showToast("success", "Inbox keys generated");
|
||||
onClose(true);
|
||||
} catch (error) {
|
||||
showToast("error", "Failed to generate inbox keys");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const trimmedPublicKey = publicKey.trim();
|
||||
const trimmedPrivateKey = privateKey.trim();
|
||||
if (!trimmedPublicKey || !trimmedPrivateKey) {
|
||||
showToast("error", "Both public and private keys are required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const isValid = await db.storage().validatePGPKeyPair({
|
||||
publicKey: trimmedPublicKey,
|
||||
privateKey: trimmedPrivateKey
|
||||
});
|
||||
if (!isValid) {
|
||||
showToast(
|
||||
"error",
|
||||
"Invalid PGP key pair. Please check your keys and try again."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialKeys) {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: "Change Inbox PGP Keys",
|
||||
message:
|
||||
"Changing Inbox PGP keys will delete all your unsynced inbox items. Are you sure?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
await db.user.saveInboxKeys({
|
||||
publicKey: trimmedPublicKey,
|
||||
privateKey: trimmedPrivateKey
|
||||
});
|
||||
showToast("success", "Inbox keys saved");
|
||||
onClose(true);
|
||||
} catch (error) {
|
||||
showToast("error", "Failed to save inbox keys");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "choose") {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title="Setup Inbox PGP Keys"
|
||||
width={500}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: () => onClose(false)
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", gap: 3 }}>
|
||||
<Text sx={{ fontSize: "body", color: "paragraph" }}>
|
||||
Choose how you want to set up your Inbox PGP keys:
|
||||
</Text>
|
||||
<Flex sx={{ flexDirection: "column", gap: 2 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleAutoGenerate}
|
||||
disabled={isLoading}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{isLoading ? "Generating..." : "Auto-generate keys"}
|
||||
</Button>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "body",
|
||||
color: "paragraph",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Or
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setMode("edit")}
|
||||
disabled={isLoading}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
Provide your own keys
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title="Inbox PGP Keys"
|
||||
width={600}
|
||||
positiveButton={{
|
||||
text: isLoading ? "Saving..." : "Save",
|
||||
onClick: handleSave,
|
||||
disabled: isLoading || !hasChanges
|
||||
}}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: () => onClose(false)
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", gap: 3 }}>
|
||||
<Field
|
||||
label="Public Key"
|
||||
id="publicKey"
|
||||
name="publicKey"
|
||||
as="textarea"
|
||||
required
|
||||
value={publicKey}
|
||||
onChange={(e) => setPublicKey(e.target.value)}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "body",
|
||||
minHeight: 150,
|
||||
resize: "vertical"
|
||||
}}
|
||||
placeholder="Enter your PGP public key..."
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Field
|
||||
label="Private Key"
|
||||
id="privateKey"
|
||||
name="privateKey"
|
||||
as="textarea"
|
||||
required
|
||||
value={privateKey}
|
||||
onChange={(e) => setPrivateKey(e.target.value)}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "body",
|
||||
minHeight: 150,
|
||||
resize: "vertical"
|
||||
}}
|
||||
placeholder="Enter your PGP private key..."
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -20,6 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { SettingsGroup } from "./types";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { InboxApiKeys } from "./components/inbox-api-keys";
|
||||
import { InboxPGPKeysDialog } from "../inbox-pgp-keys-dialog";
|
||||
import { db } from "../../common/db";
|
||||
import { showPasswordDialog } from "../password-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const InboxSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -42,6 +46,41 @@ export const InboxSettings: SettingsGroup[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "show-inbox-pgp-keys",
|
||||
title: "Inbox PGP Keys",
|
||||
description: "View/edit your Inbox PGP keys",
|
||||
keywords: ["inbox", "pgp", "keys"],
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.isInboxEnabled, listener),
|
||||
isHidden: () => !useSettingStore.getState().isInboxEnabled,
|
||||
components: [
|
||||
{
|
||||
type: "button",
|
||||
title: "Show",
|
||||
variant: "secondary",
|
||||
action: async () => {
|
||||
const ok = await showPasswordDialog({
|
||||
title: "Authenticate to view/edit Inbox PGP keys",
|
||||
inputs: {
|
||||
password: {
|
||||
label: strings.accountPassword(),
|
||||
autoComplete: "current-password"
|
||||
}
|
||||
},
|
||||
validate: ({ password }) => {
|
||||
return db.user.verifyPassword(password);
|
||||
}
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
InboxPGPKeysDialog.show({
|
||||
keys: await db.user.getInboxKeys()
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "inbox-api-keys",
|
||||
title: "",
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from "./key-value";
|
||||
import { NNCrypto } from "./nncrypto";
|
||||
import type {
|
||||
AsymmetricCipher,
|
||||
Cipher,
|
||||
SerializedKey,
|
||||
SerializedKeyPair
|
||||
@@ -34,6 +33,7 @@ import type {
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { IKeyStore } from "./key-store";
|
||||
import { User } from "@notesnook/core";
|
||||
import * as openpgp from "openpgp";
|
||||
|
||||
type EncryptedKey = { iv: Uint8Array; cipher: BufferSource };
|
||||
export type DatabasePersistence = "memory" | "db";
|
||||
@@ -133,8 +133,44 @@ export class NNStorage implements IStorage {
|
||||
return await NNCrypto.exportKey(password, salt);
|
||||
}
|
||||
|
||||
async generateCryptoKeyPair() {
|
||||
return await NNCrypto.exportKeyPair();
|
||||
async generatePGPKeyPair(): Promise<SerializedKeyPair> {
|
||||
const keys = await openpgp.generateKey({
|
||||
userIDs: [{ name: "NN", email: "NN@NN.NN" }]
|
||||
});
|
||||
return { publicKey: keys.publicKey, privateKey: keys.privateKey };
|
||||
}
|
||||
|
||||
async validatePGPKeyPair(keys: SerializedKeyPair): Promise<boolean> {
|
||||
try {
|
||||
const dummyData = JSON.stringify({
|
||||
favorite: true,
|
||||
title: "Hello world"
|
||||
});
|
||||
|
||||
const publicKey = await openpgp.readKey({ armoredKey: keys.publicKey });
|
||||
const encrypted = await openpgp.encrypt({
|
||||
message: await openpgp.createMessage({
|
||||
text: dummyData
|
||||
}),
|
||||
encryptionKeys: publicKey
|
||||
});
|
||||
|
||||
const message = await openpgp.readMessage({
|
||||
armoredMessage: encrypted
|
||||
});
|
||||
const privateKey = await openpgp.readPrivateKey({
|
||||
armoredKey: keys.privateKey
|
||||
});
|
||||
const decrypted = await openpgp.decrypt({
|
||||
message,
|
||||
decryptionKeys: privateKey
|
||||
});
|
||||
|
||||
return decrypted.data === dummyData;
|
||||
} catch (e) {
|
||||
console.error("PGP key pair validation error:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hash(password: string, email: string): Promise<string> {
|
||||
@@ -165,12 +201,21 @@ export class NNStorage implements IStorage {
|
||||
return NNCrypto.decryptMulti(key, items, "text");
|
||||
}
|
||||
|
||||
decryptAsymmetric(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<"base64">
|
||||
async decryptPGPMessage(
|
||||
privateKeyArmored: string,
|
||||
encryptedMessage: string
|
||||
): Promise<string> {
|
||||
cipherData.format = "base64";
|
||||
return NNCrypto.decryptAsymmetric(keyPair, cipherData, "base64");
|
||||
const message = await openpgp.readMessage({
|
||||
armoredMessage: encryptedMessage
|
||||
});
|
||||
const privateKey = await openpgp.readPrivateKey({
|
||||
armoredKey: privateKeyArmored
|
||||
});
|
||||
const decrypted = await openpgp.decrypt({
|
||||
message,
|
||||
decryptionKeys: privateKey
|
||||
});
|
||||
return decrypted.data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -240,7 +240,7 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
);
|
||||
};
|
||||
|
||||
init = () => {
|
||||
init = async () => {
|
||||
useSettingStore.subscribe(
|
||||
(s) => s.hideNoteTitle,
|
||||
(state) => {
|
||||
@@ -533,11 +533,11 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (activeTabId) {
|
||||
const tab = this.get().tabs.find((t) => t.id === activeTabId);
|
||||
if (!tab) return;
|
||||
rehydrateSession(tab.sessionId);
|
||||
await rehydrateSession(tab.sessionId);
|
||||
} else newSession();
|
||||
};
|
||||
|
||||
private rehydrateSession = (sessionId: string) => {
|
||||
private rehydrateSession = async (sessionId: string) => {
|
||||
const { openSession, openDiffSession, getSession, activateSession } =
|
||||
this.get();
|
||||
|
||||
@@ -549,9 +549,9 @@ class EditorStore extends BaseStore<EditorStore> {
|
||||
if (!session || !session.needsHydration) return;
|
||||
|
||||
if (session.type === "diff")
|
||||
openDiffSession(session.note.id, session.historySessionId);
|
||||
return openDiffSession(session.note.id, session.historySessionId);
|
||||
else
|
||||
openSession(session.note.id, {
|
||||
return openSession(session.note.id, {
|
||||
force: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -27,6 +27,8 @@ import { TimeFormat, DayFormat, WeekFormat } from "@notesnook/core";
|
||||
import { Profile, TrashCleanupInterval } from "@notesnook/core";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { ConfirmDialog } from "../dialogs/confirm";
|
||||
import * as openpgp from "openpgp";
|
||||
import { InboxPGPKeysDialog } from "../dialogs/inbox-pgp-keys-dialog";
|
||||
|
||||
export const HostIds = [
|
||||
"API_HOST",
|
||||
@@ -294,17 +296,14 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
|
||||
try {
|
||||
if (isInboxEnabled) {
|
||||
const inboxTokens = await db.inboxApiKeys.get();
|
||||
if (inboxTokens && inboxTokens.length > 0) {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: "Disable Inbox API",
|
||||
message:
|
||||
"Disabling will revoke all existing API keys, they will no longer work. Are you sure?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: "Disable Inbox API",
|
||||
message:
|
||||
"Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
await db.user.discardInboxKeys();
|
||||
this.set({ isInboxEnabled: false });
|
||||
@@ -312,8 +311,10 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.user.getInboxKeys();
|
||||
this.set({ isInboxEnabled: true });
|
||||
const ok = await InboxPGPKeysDialog.show({ keys: null });
|
||||
if (ok) {
|
||||
this.set({ isInboxEnabled: true });
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
showToast("error", e.message);
|
||||
|
||||
@@ -187,24 +187,11 @@ export async function handleInboxItems(
|
||||
continue;
|
||||
}
|
||||
|
||||
const decryptedKey = await db.storage().decryptAsymmetric(inboxKeys, {
|
||||
alg: item.key.alg,
|
||||
cipher: item.key.cipher,
|
||||
format: "base64",
|
||||
length: item.key.length
|
||||
});
|
||||
const decryptedItem = await db.storage().decrypt(
|
||||
{ key: decryptedKey },
|
||||
{
|
||||
alg: item.alg,
|
||||
iv: item.iv,
|
||||
cipher: item.cipher,
|
||||
format: "base64",
|
||||
length: item.length,
|
||||
salt: item.salt
|
||||
}
|
||||
);
|
||||
const decryptedItem = await db
|
||||
.storage()
|
||||
.decryptPGPMessage(inboxKeys.privateKey, item.cipher);
|
||||
const parsed = JSON.parse(decryptedItem) as ParsedInboxItem;
|
||||
|
||||
if (parsed.type !== "note") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,11 @@ export type SyncTransferItem = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type SyncInboxItem = Omit<SyncItem, "format"> & {
|
||||
key: Omit<Cipher<"base64">, "format" | "salt" | "iv">;
|
||||
export type SyncInboxItem = {
|
||||
id: string;
|
||||
v: number;
|
||||
cipher: string;
|
||||
alg: string;
|
||||
};
|
||||
|
||||
export type ParsedInboxItem = {
|
||||
|
||||
@@ -501,7 +501,7 @@ class UserManager {
|
||||
|
||||
async getInboxKeys() {
|
||||
return this.getUserKey("inboxKeys", {
|
||||
generateKey: () => this.db.crypto().generateCryptoKeyPair(),
|
||||
generateKey: () => this.db.crypto().generatePGPKeyPair(),
|
||||
errorContext: "inbox encryption keys"
|
||||
});
|
||||
}
|
||||
@@ -529,6 +529,21 @@ class UserManager {
|
||||
await this.setUser({ ...user, inboxKeys: undefined });
|
||||
}
|
||||
|
||||
async saveInboxKeys(keys: SerializedKeyPair) {
|
||||
const userEncryptionKey = await this.getMasterKey();
|
||||
if (!userEncryptionKey) return;
|
||||
|
||||
const updatePayload = {
|
||||
inboxKeys: {
|
||||
public: keys.publicKey,
|
||||
private: await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(keys.privateKey))
|
||||
}
|
||||
};
|
||||
await this.updateUser(updatePayload);
|
||||
}
|
||||
|
||||
async sendVerificationEmail(newEmail?: string) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
AsymmetricCipher,
|
||||
Cipher,
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
@@ -63,10 +62,6 @@ export interface IStorage {
|
||||
key: SerializedKey,
|
||||
items: Cipher<"base64">[]
|
||||
): Promise<string[]>;
|
||||
decryptAsymmetric(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<"base64">
|
||||
): Promise<string>;
|
||||
deriveCryptoKey(credentials: SerializedKey): Promise<void>;
|
||||
hash(
|
||||
password: string,
|
||||
@@ -75,7 +70,12 @@ export interface IStorage {
|
||||
): Promise<string>;
|
||||
getCryptoKey(): Promise<string | undefined>;
|
||||
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
|
||||
generateCryptoKeyPair(): Promise<SerializedKeyPair>;
|
||||
generatePGPKeyPair(): Promise<SerializedKeyPair>;
|
||||
decryptPGPMessage(
|
||||
privateKeyArmored: string,
|
||||
encryptedMessage: string
|
||||
): Promise<string>;
|
||||
validatePGPKeyPair(keys: SerializedKeyPair): Promise<boolean>;
|
||||
|
||||
generateCryptoKeyFallback(
|
||||
password: string,
|
||||
|
||||
@@ -30,8 +30,8 @@ export class Crypto {
|
||||
return await this.storage().generateCryptoKey(password);
|
||||
}
|
||||
|
||||
async generateCryptoKeyPair() {
|
||||
return await this.storage().generateCryptoKeyPair();
|
||||
async generatePGPKeyPair() {
|
||||
return await this.storage().generatePGPKeyPair();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,24 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const InternalLinkTypes = ["note"] as const;
|
||||
const InternalLinkTypes = ["note", "notebook", "tag", "color"] as const;
|
||||
type InternalLinkType = (typeof InternalLinkTypes)[number];
|
||||
export type InternalLink<T extends InternalLinkType = InternalLinkType> = {
|
||||
type NoteLink = BaseInternalLink<"note">;
|
||||
type NotebookLink = BaseInternalLink<"notebook">;
|
||||
type TagLink = BaseInternalLink<"tag">;
|
||||
type ColorLink = BaseInternalLink<"color">;
|
||||
type BaseInternalLink<
|
||||
T extends InternalLinkType = InternalLinkType,
|
||||
TParams extends InternalLinkParams[T] = InternalLinkParams[T]
|
||||
> = {
|
||||
type: T;
|
||||
id: string;
|
||||
params?: Partial<InternalLinkParams[T]>;
|
||||
params?: Partial<TParams>;
|
||||
};
|
||||
export type InternalLink = NoteLink | NotebookLink | TagLink | ColorLink;
|
||||
export type InternalLinkWithOffset<
|
||||
T extends InternalLinkType = InternalLinkType
|
||||
> = InternalLink<T> & {
|
||||
> = BaseInternalLink<T> & {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
@@ -34,6 +42,9 @@ export type InternalLinkWithOffset<
|
||||
|
||||
type InternalLinkParams = {
|
||||
note: { blockId: string };
|
||||
notebook: {};
|
||||
tag: {};
|
||||
color: {};
|
||||
};
|
||||
export function createInternalLink<T extends InternalLinkType>(
|
||||
type: T,
|
||||
|
||||
@@ -19,19 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ISodium } from "@notesnook/sodium";
|
||||
import KeyUtils, { base64_variants } from "./keyutils.js";
|
||||
import {
|
||||
Cipher,
|
||||
Output,
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
SerializedKeyPair,
|
||||
AsymmetricCipher
|
||||
} from "./types.js";
|
||||
import { Cipher, Output, DataFormat, SerializedKey } from "./types.js";
|
||||
|
||||
export default class Decryption {
|
||||
private static transformInput(
|
||||
sodium: ISodium,
|
||||
cipherData: Cipher<DataFormat> | AsymmetricCipher<DataFormat>
|
||||
cipherData: Cipher<DataFormat>
|
||||
): Uint8Array {
|
||||
let input: Uint8Array | null = null;
|
||||
if (
|
||||
@@ -80,28 +73,6 @@ export default class Decryption {
|
||||
) as Output<TOutputFormat>;
|
||||
}
|
||||
|
||||
static decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
sodium: ISodium,
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat: TOutputFormat = "text" as TOutputFormat
|
||||
): Output<TOutputFormat> {
|
||||
const input = this.transformInput(sodium, cipherData);
|
||||
const plaintext = sodium.crypto_box_seal_open(
|
||||
input,
|
||||
sodium.from_base64(keyPair.publicKey),
|
||||
sodium.from_base64(keyPair.privateKey)
|
||||
);
|
||||
|
||||
return (
|
||||
outputFormat === "base64"
|
||||
? sodium.to_base64(plaintext, base64_variants.URLSAFE_NO_PADDING)
|
||||
: outputFormat === "text"
|
||||
? sodium.to_string(plaintext)
|
||||
: plaintext
|
||||
) as Output<TOutputFormat>;
|
||||
}
|
||||
|
||||
static createStream(
|
||||
sodium: ISodium,
|
||||
header: string,
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
SerializedKeyPair,
|
||||
EncryptionKeyPair,
|
||||
AsymmetricCipher
|
||||
EncryptionKeyPair
|
||||
} from "./types.js";
|
||||
|
||||
export class NNCrypto implements INNCrypto {
|
||||
@@ -98,20 +97,6 @@ export class NNCrypto implements INNCrypto {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
async decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat: TOutputFormat = "text" as TOutputFormat
|
||||
): Promise<Output<TOutputFormat>> {
|
||||
await this.init();
|
||||
return Decryption.decryptAsymmetric(
|
||||
this.sodium,
|
||||
keyPair,
|
||||
cipherData,
|
||||
outputFormat
|
||||
);
|
||||
}
|
||||
|
||||
async hash(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
return Password.hash(this.sodium, password, salt);
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
Output,
|
||||
Input,
|
||||
EncryptionKeyPair,
|
||||
SerializedKeyPair,
|
||||
AsymmetricCipher
|
||||
SerializedKeyPair
|
||||
} from "./types.js";
|
||||
|
||||
export interface IStreamable {
|
||||
@@ -62,12 +61,6 @@ export interface INNCrypto {
|
||||
outputFormat?: TOutputFormat
|
||||
): Promise<Output<TOutputFormat>[]>;
|
||||
|
||||
decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat?: TOutputFormat
|
||||
): Promise<Output<TOutputFormat>>;
|
||||
|
||||
hash(password: string, salt: string): Promise<string>;
|
||||
|
||||
deriveKey(password: string, salt?: string): Promise<EncryptionKey>;
|
||||
|
||||
@@ -30,11 +30,6 @@ export type Cipher<TFormat extends DataFormat> = {
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type AsymmetricCipher<TFormat extends DataFormat> = Omit<
|
||||
Cipher<TFormat>,
|
||||
"iv" | "salt"
|
||||
>;
|
||||
|
||||
export type Output<TFormat extends DataFormat> =
|
||||
TFormat extends StringOutputFormat ? string : Uint8Array;
|
||||
export type Input<TFormat extends DataFormat> = Output<TFormat>;
|
||||
|
||||
@@ -607,6 +607,22 @@ msgstr "{words, plural, other {# selected}}"
|
||||
msgid "#notesnook"
|
||||
msgstr "#notesnook"
|
||||
|
||||
#: src/strings.ts:2662
|
||||
msgid "1 day"
|
||||
msgstr "1 day"
|
||||
|
||||
#: src/strings.ts:2664
|
||||
msgid "1 month"
|
||||
msgstr "1 month"
|
||||
|
||||
#: src/strings.ts:2663
|
||||
msgid "1 week"
|
||||
msgstr "1 week"
|
||||
|
||||
#: src/strings.ts:2665
|
||||
msgid "1 year"
|
||||
msgstr "1 year"
|
||||
|
||||
#: src/strings.ts:1583
|
||||
msgid "12-hour"
|
||||
msgstr "12-hour"
|
||||
@@ -699,6 +715,10 @@ msgstr "Add a tag"
|
||||
msgid "Add color"
|
||||
msgstr "Add color"
|
||||
|
||||
#: src/strings.ts:2650
|
||||
msgid "Add key"
|
||||
msgstr "Add key"
|
||||
|
||||
#: src/strings.ts:895
|
||||
msgid "Add notebook"
|
||||
msgstr "Add notebook"
|
||||
@@ -867,6 +887,26 @@ msgstr "and we will manually confirm your account."
|
||||
msgid "ANNOUNCEMENT"
|
||||
msgstr "ANNOUNCEMENT"
|
||||
|
||||
#: src/strings.ts:2680
|
||||
msgid "API key copied to clipboard"
|
||||
msgstr "API key copied to clipboard"
|
||||
|
||||
#: src/strings.ts:2658
|
||||
msgid "API key created successfully"
|
||||
msgstr "API key created successfully"
|
||||
|
||||
#: src/strings.ts:2685
|
||||
msgid "API key revoked"
|
||||
msgstr "API key revoked"
|
||||
|
||||
#: src/strings.ts:2651
|
||||
msgid "API Keys"
|
||||
msgstr "API Keys"
|
||||
|
||||
#: src/strings.ts:2672
|
||||
msgid "API Keys Limit Reached"
|
||||
msgstr "API Keys Limit Reached"
|
||||
|
||||
#: src/strings.ts:252
|
||||
msgid "App data has been cleared. Kindly relaunch the app to login again."
|
||||
msgstr "App data has been cleared. Kindly relaunch the app to login again."
|
||||
@@ -946,6 +986,10 @@ msgstr "Are you sure you want to remove your name?"
|
||||
msgid "Are you sure you want to remove your profile picture?"
|
||||
msgstr "Are you sure you want to remove your profile picture?"
|
||||
|
||||
#: src/strings.ts:2684
|
||||
msgid "Are you sure you want to revoke the key \"{name}\"? All inbox actions using this key will stop working immediately."
|
||||
msgstr "Are you sure you want to revoke the key \"{name}\"? All inbox actions using this key will stop working immediately."
|
||||
|
||||
#: src/strings.ts:1323
|
||||
msgid "Are you sure?"
|
||||
msgstr "Are you sure?"
|
||||
@@ -1024,6 +1068,14 @@ msgstr "Audios"
|
||||
msgid "Auth server"
|
||||
msgstr "Auth server"
|
||||
|
||||
#: src/strings.ts:2678
|
||||
msgid "Authenticate"
|
||||
msgstr "Authenticate"
|
||||
|
||||
#: src/strings.ts:2675
|
||||
msgid "Authenticate to view API key"
|
||||
msgstr "Authenticate to view API key"
|
||||
|
||||
#: src/strings.ts:1795
|
||||
msgid "Authenticated as {email}"
|
||||
msgstr "Authenticated as {email}"
|
||||
@@ -1305,6 +1357,10 @@ msgstr "Cancel subscription"
|
||||
msgid "Cancel upload"
|
||||
msgstr "Cancel upload"
|
||||
|
||||
#: src/strings.ts:2674
|
||||
msgid "Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones."
|
||||
msgstr "Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones."
|
||||
|
||||
#: src/strings.ts:2301
|
||||
msgid "Cell background color"
|
||||
msgstr "Cell background color"
|
||||
@@ -1406,6 +1462,10 @@ msgstr "Change your primary two-factor authentication method"
|
||||
msgid "Changes from other devices won't be updated in the editor in real-time."
|
||||
msgstr "Changes from other devices won't be updated in the editor in real-time."
|
||||
|
||||
#: src/strings.ts:2694
|
||||
msgid "Changing Inbox PGP keys will delete all your unsynced inbox items."
|
||||
msgstr "Changing Inbox PGP keys will delete all your unsynced inbox items."
|
||||
|
||||
#: src/strings.ts:734
|
||||
msgid "Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
|
||||
msgstr "Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
|
||||
@@ -1890,6 +1950,14 @@ msgstr "Create a tag to group related notes together."
|
||||
msgid "Create account"
|
||||
msgstr "Create account"
|
||||
|
||||
#: src/strings.ts:2653
|
||||
msgid "Create API Key"
|
||||
msgstr "Create API Key"
|
||||
|
||||
#: src/strings.ts:2670
|
||||
msgid "Create Key"
|
||||
msgstr "Create Key"
|
||||
|
||||
#: src/strings.ts:581
|
||||
msgid "Create link"
|
||||
msgstr "Create link"
|
||||
@@ -1918,15 +1986,27 @@ msgstr "Create vault"
|
||||
msgid "Create your account"
|
||||
msgstr "Create your account"
|
||||
|
||||
#: src/strings.ts:2669
|
||||
msgid "Create your first api key to get started."
|
||||
msgstr "Create your first api key to get started."
|
||||
|
||||
#: src/strings.ts:2229
|
||||
msgid "Created at"
|
||||
msgstr "Created at"
|
||||
|
||||
#: src/strings.ts:2689
|
||||
msgid "Created on"
|
||||
msgstr "Created on"
|
||||
|
||||
#. placeholder {0}: type === "full" ? " full" : ""
|
||||
#: src/strings.ts:1344
|
||||
msgid "Creating a{0} backup"
|
||||
msgstr "Creating a{0} backup"
|
||||
|
||||
#: src/strings.ts:2661
|
||||
msgid "Creating..."
|
||||
msgstr "Creating..."
|
||||
|
||||
#: src/strings.ts:2049
|
||||
msgid "Credentials"
|
||||
msgstr "Credentials"
|
||||
@@ -2205,6 +2285,10 @@ msgstr "Disable auto sync"
|
||||
msgid "Disable editor margins"
|
||||
msgstr "Disable editor margins"
|
||||
|
||||
#: src/strings.ts:2647
|
||||
msgid "Disable Inbox API"
|
||||
msgstr "Disable Inbox API"
|
||||
|
||||
#: src/strings.ts:1087
|
||||
msgid "Disable realtime sync"
|
||||
msgstr "Disable realtime sync"
|
||||
@@ -2217,6 +2301,10 @@ msgstr "Disable sync"
|
||||
msgid "Disabled"
|
||||
msgstr "Disabled"
|
||||
|
||||
#: src/strings.ts:2649
|
||||
msgid "Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?"
|
||||
msgstr "Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?"
|
||||
|
||||
#: src/strings.ts:565
|
||||
msgid "Discard"
|
||||
msgstr "Discard"
|
||||
@@ -2382,6 +2470,10 @@ msgstr "Due today"
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplicate"
|
||||
|
||||
#: src/strings.ts:2655
|
||||
msgid "e.g., Todo integration"
|
||||
msgstr "e.g., Todo integration"
|
||||
|
||||
#: src/strings.ts:644
|
||||
msgid "Earliest first"
|
||||
msgstr "Earliest first"
|
||||
@@ -2484,6 +2576,10 @@ msgstr "Enable app lock"
|
||||
msgid "Enable editor margins"
|
||||
msgstr "Enable editor margins"
|
||||
|
||||
#: src/strings.ts:2642
|
||||
msgid "Enable Inbox API"
|
||||
msgstr "Enable Inbox API"
|
||||
|
||||
#: src/strings.ts:2465
|
||||
msgid "Enable ligatures for common symbols like →, ←, etc"
|
||||
msgstr "Enable ligatures for common symbols like →, ←, etc"
|
||||
@@ -2500,6 +2596,10 @@ msgstr "Enable spell checker"
|
||||
msgid "Enable two-factor authentication to add an extra layer of security to your account."
|
||||
msgstr "Enable two-factor authentication to add an extra layer of security to your account."
|
||||
|
||||
#: src/strings.ts:2643
|
||||
msgid "Enable/Disable Inbox API"
|
||||
msgstr "Enable/Disable Inbox API"
|
||||
|
||||
#: src/strings.ts:1234
|
||||
msgid "Encrypt your backups for added security"
|
||||
msgstr "Encrypt your backups for added security"
|
||||
@@ -2708,6 +2808,18 @@ msgstr "Expand sidebar"
|
||||
msgid "Experience the next level of private note taking\""
|
||||
msgstr "Experience the next level of private note taking\""
|
||||
|
||||
#: src/strings.ts:2691
|
||||
msgid "Expired"
|
||||
msgstr "Expired"
|
||||
|
||||
#: src/strings.ts:2656
|
||||
msgid "Expires in"
|
||||
msgstr "Expires in"
|
||||
|
||||
#: src/strings.ts:2692
|
||||
msgid "Expires on"
|
||||
msgstr "Expires on"
|
||||
|
||||
#: src/strings.ts:2632
|
||||
msgid "Expiry date"
|
||||
msgstr "Expiry date"
|
||||
@@ -2773,6 +2885,15 @@ msgstr "Failed to attach file"
|
||||
msgid "Failed to copy note"
|
||||
msgstr "Failed to copy note"
|
||||
|
||||
#: src/strings.ts:2681
|
||||
msgid "Failed to copy to clipboard"
|
||||
msgstr "Failed to copy to clipboard"
|
||||
|
||||
#. placeholder {0}: message ? `: ${message}` : ""
|
||||
#: src/strings.ts:2660
|
||||
msgid "Failed to create API key{0}"
|
||||
msgstr "Failed to create API key{0}"
|
||||
|
||||
#: src/strings.ts:1560
|
||||
msgid "Failed to decrypt backup"
|
||||
msgstr "Failed to decrypt backup"
|
||||
@@ -2793,6 +2914,10 @@ msgstr "Failed to download file"
|
||||
msgid "Failed to install theme."
|
||||
msgstr "Failed to install theme."
|
||||
|
||||
#: src/strings.ts:2667
|
||||
msgid "Failed to load API keys. Please try again."
|
||||
msgstr "Failed to load API keys. Please try again."
|
||||
|
||||
#: src/strings.ts:948
|
||||
msgid "Failed to open"
|
||||
msgstr "Failed to open"
|
||||
@@ -2809,6 +2934,10 @@ msgstr "Failed to register task"
|
||||
msgid "Failed to resolve download url"
|
||||
msgstr "Failed to resolve download url"
|
||||
|
||||
#: src/strings.ts:2686
|
||||
msgid "Failed to revoke API key"
|
||||
msgstr "Failed to revoke API key"
|
||||
|
||||
#: src/strings.ts:773
|
||||
msgid "Failed to send recovery email"
|
||||
msgstr "Failed to send recovery email"
|
||||
@@ -3364,6 +3493,18 @@ msgstr "Import CSV"
|
||||
msgid "import guide"
|
||||
msgstr "import guide"
|
||||
|
||||
#: src/strings.ts:2639
|
||||
msgid "Inbox API"
|
||||
msgstr "Inbox API"
|
||||
|
||||
#: src/strings.ts:2644
|
||||
msgid "Inbox Keys"
|
||||
msgstr "Inbox Keys"
|
||||
|
||||
#: src/strings.ts:2699
|
||||
msgid "Inbox keys saved"
|
||||
msgstr "Inbox keys saved"
|
||||
|
||||
#: src/strings.ts:174
|
||||
msgid "Incoming"
|
||||
msgstr "Incoming"
|
||||
@@ -3444,6 +3585,14 @@ msgstr "Invalid CORS proxy url"
|
||||
msgid "Invalid email"
|
||||
msgstr "Invalid email"
|
||||
|
||||
#: src/strings.ts:2679
|
||||
msgid "Invalid password"
|
||||
msgstr "Invalid password"
|
||||
|
||||
#: src/strings.ts:2698
|
||||
msgid "Invalid PGP key pair. Please check your keys and try again."
|
||||
msgstr "Invalid PGP key pair. Please check your keys and try again."
|
||||
|
||||
#: src/strings.ts:224
|
||||
msgid "Issue created"
|
||||
msgstr "Issue created"
|
||||
@@ -3526,6 +3675,10 @@ msgstr "Keep open"
|
||||
msgid "Keep your data safe"
|
||||
msgstr "Keep your data safe"
|
||||
|
||||
#: src/strings.ts:2654
|
||||
msgid "Key name"
|
||||
msgstr "Key name"
|
||||
|
||||
#: src/strings.ts:2128
|
||||
msgid "Languages"
|
||||
msgstr "Languages"
|
||||
@@ -3534,6 +3687,10 @@ msgstr "Languages"
|
||||
msgid "Last edited at"
|
||||
msgstr "Last edited at"
|
||||
|
||||
#: src/strings.ts:2687
|
||||
msgid "Last used on"
|
||||
msgstr "Last used on"
|
||||
|
||||
#: src/strings.ts:590
|
||||
msgid "Later"
|
||||
msgstr "Later"
|
||||
@@ -3659,6 +3816,10 @@ msgstr "Loading"
|
||||
msgid "Loading {0}, please wait..."
|
||||
msgstr "Loading {0}, please wait..."
|
||||
|
||||
#: src/strings.ts:2666
|
||||
msgid "Loading API keys..."
|
||||
msgstr "Loading API keys..."
|
||||
|
||||
#: src/strings.ts:1664
|
||||
msgid "Loading editor. Please wait..."
|
||||
msgstr "Loading editor. Please wait..."
|
||||
@@ -4024,6 +4185,10 @@ msgstr "Never"
|
||||
msgid "Never ask again"
|
||||
msgstr "Never ask again"
|
||||
|
||||
#: src/strings.ts:2690
|
||||
msgid "Never expires"
|
||||
msgstr "Never expires"
|
||||
|
||||
#: src/strings.ts:1037
|
||||
msgid "Never hesitate to choose privacy"
|
||||
msgstr "Never hesitate to choose privacy"
|
||||
@@ -4032,6 +4197,10 @@ msgstr "Never hesitate to choose privacy"
|
||||
msgid "Never show again"
|
||||
msgstr "Never show again"
|
||||
|
||||
#: src/strings.ts:2688
|
||||
msgid "Never used"
|
||||
msgstr "Never used"
|
||||
|
||||
#: src/strings.ts:642
|
||||
msgid "New - old"
|
||||
msgstr "New - old"
|
||||
@@ -4369,6 +4538,10 @@ msgstr "Off"
|
||||
msgid "Offline"
|
||||
msgstr "Offline"
|
||||
|
||||
#: src/strings.ts:2671
|
||||
msgid "OK"
|
||||
msgstr "OK"
|
||||
|
||||
#: src/strings.ts:2227
|
||||
msgid "Okay"
|
||||
msgstr "Okay"
|
||||
@@ -4640,6 +4813,10 @@ msgstr "Please download a backup of your data as your account will be cleared be
|
||||
msgid "Please enable automatic backups to avoid losing important data."
|
||||
msgstr "Please enable automatic backups to avoid losing important data."
|
||||
|
||||
#: src/strings.ts:2657
|
||||
msgid "Please enter a key name"
|
||||
msgstr "Please enter a key name"
|
||||
|
||||
#: src/strings.ts:1512
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr "Please enter a valid email address"
|
||||
@@ -4676,6 +4853,10 @@ msgstr "Please enter the password to unlock this note"
|
||||
msgid "Please enter the password to view this version"
|
||||
msgstr "Please enter the password to view this version"
|
||||
|
||||
#: src/strings.ts:2677
|
||||
msgid "Please enter your account password to view this API key."
|
||||
msgstr "Please enter your account password to view this API key."
|
||||
|
||||
#: src/strings.ts:1021
|
||||
msgid "Please enter your app lock password to continue"
|
||||
msgstr "Please enter your app lock password to continue"
|
||||
@@ -4882,6 +5063,10 @@ msgstr "Privacy Policy. "
|
||||
msgid "private analytics and bug reports."
|
||||
msgstr "private analytics and bug reports."
|
||||
|
||||
#: src/strings.ts:2696
|
||||
msgid "Private Key:"
|
||||
msgstr "Private Key:"
|
||||
|
||||
#: src/strings.ts:821
|
||||
msgid "Private."
|
||||
msgstr "Private."
|
||||
@@ -4930,6 +5115,10 @@ msgstr "Protect your notes"
|
||||
msgid "Proxy"
|
||||
msgstr "Proxy"
|
||||
|
||||
#: src/strings.ts:2695
|
||||
msgid "Public Key:"
|
||||
msgstr "Public Key:"
|
||||
|
||||
#: src/strings.ts:486
|
||||
msgid "Publish"
|
||||
msgstr "Publish"
|
||||
@@ -5421,6 +5610,10 @@ msgstr "Resubscribe from Playstore"
|
||||
msgid "Resubscribe to Pro"
|
||||
msgstr "Resubscribe to Pro"
|
||||
|
||||
#: src/strings.ts:2668
|
||||
msgid "Retry"
|
||||
msgstr "Retry"
|
||||
|
||||
#: src/strings.ts:513
|
||||
msgid "Reupload"
|
||||
msgstr "Reupload"
|
||||
@@ -5437,6 +5630,10 @@ msgstr "Revoke"
|
||||
msgid "Revoke biometric unlocking"
|
||||
msgstr "Revoke biometric unlocking"
|
||||
|
||||
#: src/strings.ts:2682
|
||||
msgid "Revoke Inbox API Key - {name}"
|
||||
msgstr "Revoke Inbox API Key - {name}"
|
||||
|
||||
#: src/strings.ts:450
|
||||
msgid "Revoke vault fingerprint unlock"
|
||||
msgstr "Revoke vault fingerprint unlock"
|
||||
@@ -5988,6 +6185,10 @@ msgstr "Share note"
|
||||
msgid "Share Notesnook with friends!"
|
||||
msgstr "Share Notesnook with friends!"
|
||||
|
||||
#: src/strings.ts:2641
|
||||
msgid "Share things to Notesbook from anywhere using the Inbox API"
|
||||
msgstr "Share things to Notesbook from anywhere using the Inbox API"
|
||||
|
||||
#: src/strings.ts:594
|
||||
msgid "Share to cloud"
|
||||
msgstr "Share to cloud"
|
||||
@@ -7108,6 +7309,14 @@ msgstr "Videos"
|
||||
msgid "View all linked notebooks"
|
||||
msgstr "View all linked notebooks"
|
||||
|
||||
#: src/strings.ts:2646
|
||||
msgid "View and edit your inbox public/private key pair"
|
||||
msgstr "View and edit your inbox public/private key pair"
|
||||
|
||||
#: src/strings.ts:2652
|
||||
msgid "View and manage inbox API keys"
|
||||
msgstr "View and manage inbox API keys"
|
||||
|
||||
#: src/strings.ts:1270
|
||||
msgid "View and share debug logs"
|
||||
msgstr "View and share debug logs"
|
||||
|
||||
@@ -607,6 +607,22 @@ msgstr ""
|
||||
msgid "#notesnook"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2662
|
||||
msgid "1 day"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2664
|
||||
msgid "1 month"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2663
|
||||
msgid "1 week"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2665
|
||||
msgid "1 year"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1583
|
||||
msgid "12-hour"
|
||||
msgstr ""
|
||||
@@ -699,6 +715,10 @@ msgstr ""
|
||||
msgid "Add color"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2650
|
||||
msgid "Add key"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:895
|
||||
msgid "Add notebook"
|
||||
msgstr ""
|
||||
@@ -867,6 +887,26 @@ msgstr ""
|
||||
msgid "ANNOUNCEMENT"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2680
|
||||
msgid "API key copied to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2658
|
||||
msgid "API key created successfully"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2685
|
||||
msgid "API key revoked"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2651
|
||||
msgid "API Keys"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2672
|
||||
msgid "API Keys Limit Reached"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:252
|
||||
msgid "App data has been cleared. Kindly relaunch the app to login again."
|
||||
msgstr ""
|
||||
@@ -946,6 +986,10 @@ msgstr ""
|
||||
msgid "Are you sure you want to remove your profile picture?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2684
|
||||
msgid "Are you sure you want to revoke the key \"{name}\"? All inbox actions using this key will stop working immediately."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1323
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
@@ -1024,6 +1068,14 @@ msgstr ""
|
||||
msgid "Auth server"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2678
|
||||
msgid "Authenticate"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2675
|
||||
msgid "Authenticate to view API key"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1795
|
||||
msgid "Authenticated as {email}"
|
||||
msgstr ""
|
||||
@@ -1305,6 +1357,10 @@ msgstr ""
|
||||
msgid "Cancel upload"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2674
|
||||
msgid "Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2301
|
||||
msgid "Cell background color"
|
||||
msgstr ""
|
||||
@@ -1406,6 +1462,10 @@ msgstr ""
|
||||
msgid "Changes from other devices won't be updated in the editor in real-time."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2694
|
||||
msgid "Changing Inbox PGP keys will delete all your unsynced inbox items."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:734
|
||||
msgid "Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection."
|
||||
msgstr ""
|
||||
@@ -1879,6 +1939,14 @@ msgstr ""
|
||||
msgid "Create account"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2653
|
||||
msgid "Create API Key"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2670
|
||||
msgid "Create Key"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:581
|
||||
msgid "Create link"
|
||||
msgstr ""
|
||||
@@ -1907,15 +1975,27 @@ msgstr ""
|
||||
msgid "Create your account"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2669
|
||||
msgid "Create your first api key to get started."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2229
|
||||
msgid "Created at"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2689
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: type === "full" ? " full" : ""
|
||||
#: src/strings.ts:1344
|
||||
msgid "Creating a{0} backup"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2661
|
||||
msgid "Creating..."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2049
|
||||
msgid "Credentials"
|
||||
msgstr ""
|
||||
@@ -2194,6 +2274,10 @@ msgstr ""
|
||||
msgid "Disable editor margins"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2647
|
||||
msgid "Disable Inbox API"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1087
|
||||
msgid "Disable realtime sync"
|
||||
msgstr ""
|
||||
@@ -2206,6 +2290,10 @@ msgstr ""
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2649
|
||||
msgid "Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:565
|
||||
msgid "Discard"
|
||||
msgstr ""
|
||||
@@ -2371,6 +2459,10 @@ msgstr ""
|
||||
msgid "Duplicate"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2655
|
||||
msgid "e.g., Todo integration"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:644
|
||||
msgid "Earliest first"
|
||||
msgstr ""
|
||||
@@ -2473,6 +2565,10 @@ msgstr ""
|
||||
msgid "Enable editor margins"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2642
|
||||
msgid "Enable Inbox API"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2465
|
||||
msgid "Enable ligatures for common symbols like →, ←, etc"
|
||||
msgstr ""
|
||||
@@ -2489,6 +2585,10 @@ msgstr ""
|
||||
msgid "Enable two-factor authentication to add an extra layer of security to your account."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2643
|
||||
msgid "Enable/Disable Inbox API"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1234
|
||||
msgid "Encrypt your backups for added security"
|
||||
msgstr ""
|
||||
@@ -2697,6 +2797,18 @@ msgstr ""
|
||||
msgid "Experience the next level of private note taking\""
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2691
|
||||
msgid "Expired"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2656
|
||||
msgid "Expires in"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2692
|
||||
msgid "Expires on"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2632
|
||||
msgid "Expiry date"
|
||||
msgstr ""
|
||||
@@ -2762,6 +2874,15 @@ msgstr ""
|
||||
msgid "Failed to copy note"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2681
|
||||
msgid "Failed to copy to clipboard"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: message ? `: ${message}` : ""
|
||||
#: src/strings.ts:2660
|
||||
msgid "Failed to create API key{0}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1560
|
||||
msgid "Failed to decrypt backup"
|
||||
msgstr ""
|
||||
@@ -2782,6 +2903,10 @@ msgstr ""
|
||||
msgid "Failed to install theme."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2667
|
||||
msgid "Failed to load API keys. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:948
|
||||
msgid "Failed to open"
|
||||
msgstr ""
|
||||
@@ -2798,6 +2923,10 @@ msgstr ""
|
||||
msgid "Failed to resolve download url"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2686
|
||||
msgid "Failed to revoke API key"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:773
|
||||
msgid "Failed to send recovery email"
|
||||
msgstr ""
|
||||
@@ -3344,6 +3473,18 @@ msgstr ""
|
||||
msgid "import guide"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2639
|
||||
msgid "Inbox API"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2644
|
||||
msgid "Inbox Keys"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2699
|
||||
msgid "Inbox keys saved"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:174
|
||||
msgid "Incoming"
|
||||
msgstr ""
|
||||
@@ -3424,6 +3565,14 @@ msgstr ""
|
||||
msgid "Invalid email"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2679
|
||||
msgid "Invalid password"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2698
|
||||
msgid "Invalid PGP key pair. Please check your keys and try again."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:224
|
||||
msgid "Issue created"
|
||||
msgstr ""
|
||||
@@ -3506,6 +3655,10 @@ msgstr ""
|
||||
msgid "Keep your data safe"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2654
|
||||
msgid "Key name"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2128
|
||||
msgid "Languages"
|
||||
msgstr ""
|
||||
@@ -3514,6 +3667,10 @@ msgstr ""
|
||||
msgid "Last edited at"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2687
|
||||
msgid "Last used on"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:590
|
||||
msgid "Later"
|
||||
msgstr ""
|
||||
@@ -3639,6 +3796,10 @@ msgstr ""
|
||||
msgid "Loading {0}, please wait..."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2666
|
||||
msgid "Loading API keys..."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1664
|
||||
msgid "Loading editor. Please wait..."
|
||||
msgstr ""
|
||||
@@ -4004,6 +4165,10 @@ msgstr ""
|
||||
msgid "Never ask again"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2690
|
||||
msgid "Never expires"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1037
|
||||
msgid "Never hesitate to choose privacy"
|
||||
msgstr ""
|
||||
@@ -4012,6 +4177,10 @@ msgstr ""
|
||||
msgid "Never show again"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2688
|
||||
msgid "Never used"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:642
|
||||
msgid "New - old"
|
||||
msgstr ""
|
||||
@@ -4343,6 +4512,10 @@ msgstr ""
|
||||
msgid "Offline"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2671
|
||||
msgid "OK"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2227
|
||||
msgid "Okay"
|
||||
msgstr ""
|
||||
@@ -4614,6 +4787,10 @@ msgstr ""
|
||||
msgid "Please enable automatic backups to avoid losing important data."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2657
|
||||
msgid "Please enter a key name"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1512
|
||||
msgid "Please enter a valid email address"
|
||||
msgstr ""
|
||||
@@ -4650,6 +4827,10 @@ msgstr ""
|
||||
msgid "Please enter the password to view this version"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2677
|
||||
msgid "Please enter your account password to view this API key."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1021
|
||||
msgid "Please enter your app lock password to continue"
|
||||
msgstr ""
|
||||
@@ -4856,6 +5037,10 @@ msgstr ""
|
||||
msgid "private analytics and bug reports."
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2696
|
||||
msgid "Private Key:"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:821
|
||||
msgid "Private."
|
||||
msgstr ""
|
||||
@@ -4904,6 +5089,10 @@ msgstr ""
|
||||
msgid "Proxy"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2695
|
||||
msgid "Public Key:"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:486
|
||||
msgid "Publish"
|
||||
msgstr ""
|
||||
@@ -5395,6 +5584,10 @@ msgstr ""
|
||||
msgid "Resubscribe to Pro"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2668
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:513
|
||||
msgid "Reupload"
|
||||
msgstr ""
|
||||
@@ -5411,6 +5604,10 @@ msgstr ""
|
||||
msgid "Revoke biometric unlocking"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2682
|
||||
msgid "Revoke Inbox API Key - {name}"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:450
|
||||
msgid "Revoke vault fingerprint unlock"
|
||||
msgstr ""
|
||||
@@ -5954,6 +6151,10 @@ msgstr ""
|
||||
msgid "Share Notesnook with friends!"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2641
|
||||
msgid "Share things to Notesbook from anywhere using the Inbox API"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:594
|
||||
msgid "Share to cloud"
|
||||
msgstr ""
|
||||
@@ -7058,6 +7259,14 @@ msgstr ""
|
||||
msgid "View all linked notebooks"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2646
|
||||
msgid "View and edit your inbox public/private key pair"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:2652
|
||||
msgid "View and manage inbox API keys"
|
||||
msgstr ""
|
||||
|
||||
#: src/strings.ts:1270
|
||||
msgid "View and share debug logs"
|
||||
msgstr ""
|
||||
|
||||
@@ -2636,5 +2636,66 @@ Use this if changes from other devices are not appearing on this device. This wi
|
||||
deleteData: () => t`Delete data`,
|
||||
failedToAttachFile: () => t`Failed to attach file`,
|
||||
unlockNoteToMergeConflicts: () => t`Unlock note to merge conflicts`,
|
||||
confirmationEmailSent: () => t`Confirmation email sent`
|
||||
confirmationEmailSent: () => t`Confirmation email sent`,
|
||||
inboxAPI: () => t`Inbox API`,
|
||||
inboxAPIDesc: () =>
|
||||
t`Share things to Notesbook from anywhere using the Inbox API`,
|
||||
enableInboxAPI: () => t`Enable Inbox API`,
|
||||
enableInboxAPIDesc: () => t`Enable/Disable Inbox API`,
|
||||
manageInboxKeys: () => t`Inbox Keys`,
|
||||
manageInboxKeysDesc: () =>
|
||||
t`View and edit your inbox public/private key pair`,
|
||||
disableInboxAPI: () => t`Disable Inbox API`,
|
||||
disableInboxAPIDesc: () =>
|
||||
t`Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?`,
|
||||
addKey: () => t`Add key`,
|
||||
viewAPIKeys: () => t`API Keys`,
|
||||
viewAPIKeysDesc: () => t`View and manage inbox API keys`,
|
||||
createApiKey: () => t`Create API Key`,
|
||||
keyName: () => t`Key name`,
|
||||
exampleKeyName: () => t`e.g., Todo integration`,
|
||||
expiresIn: () => t`Expires in`,
|
||||
enterKeyName: () => t`Please enter a key name`,
|
||||
apiKeyCreatedSuccessfully: () => t`API key created successfully`,
|
||||
failedToCreateApiKey: (message: string) =>
|
||||
t`Failed to create API key${message ? `: ${message}` : ""}`,
|
||||
creating: () => t`Creating...`,
|
||||
expiryOneDay: () => t`1 day`,
|
||||
expiryOneWeek: () => t`1 week`,
|
||||
expiryOneMonth: () => t`1 month`,
|
||||
expiryOneYear: () => t`1 year`,
|
||||
loadingApiKeys: () => t`Loading API keys...`,
|
||||
failedToLoadApiKeys: () => t`Failed to load API keys. Please try again.`,
|
||||
retry: () => t`Retry`,
|
||||
createFirstApiKey: () => t`Create your first api key to get started.`,
|
||||
createKey: () => t`Create Key`,
|
||||
ok: () => t`OK`,
|
||||
apiKeysLimitReached: () => t`API Keys Limit Reached`,
|
||||
apiKeysLimitReachedMessage: () =>
|
||||
t`Cannot create more than 10 api keys at a time. Please revoke some existing keys before creating new ones.`,
|
||||
authenticateToViewApiKey: () => t`Authenticate to view API key`,
|
||||
enterPasswordToViewApiKey: () =>
|
||||
t`Please enter your account password to view this API key.`,
|
||||
authenticate: () => t`Authenticate`,
|
||||
invalidPassword: () => t`Invalid password`,
|
||||
apiKeyCopiedToClipboard: () => t`API key copied to clipboard`,
|
||||
failedToCopyToClipboard: () => t`Failed to copy to clipboard`,
|
||||
revokeInboxApiKey: (name: string) => t`Revoke Inbox API Key - ${name}`,
|
||||
revokeApiKeyConfirmation: (name: string) =>
|
||||
t`Are you sure you want to revoke the key "${name}"? All inbox actions using this key will stop working immediately.`,
|
||||
apiKeyRevoked: () => t`API key revoked`,
|
||||
failedToRevokeApiKey: () => t`Failed to revoke API key`,
|
||||
lastUsedOn: () => t`Last used on`,
|
||||
neverUsed: () => t`Never used`,
|
||||
createdOn: () => t`Created on`,
|
||||
neverExpires: () => t`Never expires`,
|
||||
expired: () => t`Expired`,
|
||||
expiresOn: () => t`Expires on`,
|
||||
changingInboxPgpKeysNotice: () =>
|
||||
t`Changing Inbox PGP keys will delete all your unsynced inbox items.`,
|
||||
publicKey: () => t`Public Key:`,
|
||||
privateKey: () => t`Private Key:`,
|
||||
invalidPgpKeyPair: () =>
|
||||
t`Invalid PGP key pair. Please check your keys and try again.`,
|
||||
inboxKeysSaved: () => t`Inbox keys saved`
|
||||
};
|
||||
|
||||
@@ -118,12 +118,6 @@ export class Sodium implements ISodium {
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() {
|
||||
return sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
}
|
||||
get crypto_box_keypair() {
|
||||
return sodium.crypto_box_keypair;
|
||||
}
|
||||
get crypto_box_seal_open() {
|
||||
return sodium.crypto_box_seal_open;
|
||||
}
|
||||
}
|
||||
|
||||
function convertVariant(variant: base64_variants): sodium.base64_variants {
|
||||
|
||||
@@ -46,12 +46,7 @@ import {
|
||||
crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
||||
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
|
||||
crypto_secretstream_xchacha20poly1305_TAG_FINAL,
|
||||
crypto_secretstream_xchacha20poly1305_TAG_MESSAGE,
|
||||
crypto_box_keypair as sodium_native_crypto_box_keypair,
|
||||
crypto_box_PUBLICKEYBYTES,
|
||||
crypto_box_SECRETKEYBYTES,
|
||||
crypto_box_seal_open as sodium_native_crypto_box_seal_open,
|
||||
crypto_box_SEALBYTES
|
||||
crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
|
||||
} from "sodium-native";
|
||||
import { Buffer } from "node:buffer";
|
||||
import { base64_variants, ISodium } from "./types";
|
||||
@@ -346,71 +341,6 @@ function crypto_secretstream_xchacha20poly1305_pull(
|
||||
return { message, tag: tag.readUInt8() } as MessageTag | StringMessageTag;
|
||||
}
|
||||
|
||||
function crypto_box_keypair(
|
||||
outputFormat?: Uint8ArrayOutputFormat | null
|
||||
): KeyPair;
|
||||
function crypto_box_keypair(outputFormat: StringOutputFormat): StringKeyPair;
|
||||
function crypto_box_keypair(
|
||||
outputFormat?: Uint8ArrayOutputFormat | null | StringOutputFormat
|
||||
): KeyPair | StringKeyPair {
|
||||
const publicBuffer = Buffer.alloc(crypto_box_PUBLICKEYBYTES);
|
||||
const privateBuffer = Buffer.alloc(crypto_box_SECRETKEYBYTES);
|
||||
|
||||
sodium_native_crypto_box_keypair(publicBuffer, privateBuffer);
|
||||
|
||||
if (typeof outputFormat === "string") {
|
||||
const transformer =
|
||||
outputFormat === "base64"
|
||||
? to_base64
|
||||
: outputFormat === "hex"
|
||||
? to_hex
|
||||
: to_string;
|
||||
return {
|
||||
keyType: "x25519" as KeyType,
|
||||
publicKey: transformer(new Uint8Array(publicBuffer)),
|
||||
privateKey: transformer(new Uint8Array(privateBuffer))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
keyType: "x25519" as KeyType,
|
||||
publicKey: new Uint8Array(publicBuffer),
|
||||
privateKey: new Uint8Array(privateBuffer)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function crypto_box_seal_open(
|
||||
ciphertext: string | Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
outputFormat?: Uint8ArrayOutputFormat | null
|
||||
): Uint8Array;
|
||||
function crypto_box_seal_open(
|
||||
ciphertext: string | Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
outputFormat: StringOutputFormat
|
||||
): string;
|
||||
function crypto_box_seal_open(
|
||||
ciphertext: string | Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
outputFormat?: StringOutputFormat | Uint8ArrayOutputFormat | null
|
||||
): string | Uint8Array {
|
||||
const cipher = toBuffer(ciphertext);
|
||||
return wrap(
|
||||
cipher.byteLength - crypto_box_SEALBYTES,
|
||||
(message) =>
|
||||
sodium_native_crypto_box_seal_open(
|
||||
message,
|
||||
cipher,
|
||||
toBuffer(publicKey),
|
||||
toBuffer(privateKey)
|
||||
),
|
||||
outputFormat
|
||||
);
|
||||
}
|
||||
|
||||
function randombytes_buf(
|
||||
length: number,
|
||||
outputFormat?: Uint8ArrayOutputFormat | null
|
||||
@@ -473,10 +403,6 @@ function to_string(input: Uint8Array): string {
|
||||
);
|
||||
}
|
||||
|
||||
function to_hex(input: Uint8Array): string {
|
||||
return Buffer.from(input, input.byteOffset, input.byteLength).toString("hex");
|
||||
}
|
||||
|
||||
type ToBufferInput = string | Uint8Array | null | undefined;
|
||||
type ToBufferResult<TInput extends ToBufferInput> = TInput extends
|
||||
| undefined
|
||||
@@ -625,12 +551,6 @@ export class Sodium implements ISodium {
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() {
|
||||
return crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
}
|
||||
get crypto_box_keypair() {
|
||||
return crypto_box_keypair;
|
||||
}
|
||||
get crypto_box_seal_open() {
|
||||
return crypto_box_seal_open;
|
||||
}
|
||||
}
|
||||
|
||||
export { base64_variants, type ISodium };
|
||||
|
||||
@@ -61,6 +61,4 @@ export interface ISodium {
|
||||
get crypto_aead_xchacha20poly1305_ietf_NPUBBYTES(): typeof sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_FINAL(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
get crypto_box_keypair(): typeof sodium.crypto_box_keypair;
|
||||
get crypto_box_seal_open(): typeof sodium.crypto_box_seal_open;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user