mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-02 09:50:47 +00:00
update ui for consistent in light and dark mode
This commit is contained in:
parent
854a47f266
commit
507628bcaa
@ -19,11 +19,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@evilmartians/harmony": "^1.1.0",
|
"@evilmartians/harmony": "^1.1.0",
|
||||||
"@formkit/auto-animate": "^0.8.0",
|
|
||||||
"@getalby/sdk": "^2.5.0",
|
"@getalby/sdk": "^2.5.0",
|
||||||
"@nostr-dev-kit/ndk": "^2.0.3",
|
"@nostr-dev-kit/ndk": "^2.0.3",
|
||||||
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.3",
|
"@nostr-dev-kit/ndk-cache-dexie": "^2.0.3",
|
||||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
"@nostr-fetch/adapter-ndk": "^0.13.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
@ -33,7 +32,7 @@
|
|||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-toolbar": "^1.0.4",
|
"@radix-ui/react-toolbar": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "4.36.1",
|
||||||
"@tauri-apps/api": "2.0.0-alpha.8",
|
"@tauri-apps/api": "2.0.0-alpha.8",
|
||||||
"@tauri-apps/cli": "2.0.0-alpha.15",
|
"@tauri-apps/cli": "2.0.0-alpha.15",
|
||||||
"@tauri-apps/plugin-app": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-app": "2.0.0-alpha.1",
|
||||||
@ -81,7 +80,6 @@
|
|||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.17.0",
|
"react-router-dom": "^6.17.0",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"react-xarrows": "^2.0.2",
|
|
||||||
"reactflow": "^11.9.4",
|
"reactflow": "^11.9.4",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sonner": "^1.0.3",
|
"sonner": "^1.0.3",
|
||||||
|
@ -8,9 +8,6 @@ dependencies:
|
|||||||
'@evilmartians/harmony':
|
'@evilmartians/harmony':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
'@formkit/auto-animate':
|
|
||||||
specifier: ^0.8.0
|
|
||||||
version: 0.8.0
|
|
||||||
'@getalby/sdk':
|
'@getalby/sdk':
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
@ -21,8 +18,8 @@ dependencies:
|
|||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.3(typescript@5.2.2)
|
version: 2.0.3(typescript@5.2.2)
|
||||||
'@nostr-fetch/adapter-ndk':
|
'@nostr-fetch/adapter-ndk':
|
||||||
specifier: ^0.12.2
|
specifier: ^0.13.0
|
||||||
version: 0.12.2(@nostr-dev-kit/ndk@2.0.3)(nostr-fetch@0.13.0)
|
version: 0.13.0(@nostr-dev-kit/ndk@2.0.3)(nostr-fetch@0.13.0)
|
||||||
'@radix-ui/react-alert-dialog':
|
'@radix-ui/react-alert-dialog':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -51,7 +48,7 @@ dependencies:
|
|||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^4.36.1
|
specifier: 4.36.1
|
||||||
version: 4.36.1(react-dom@18.2.0)(react@18.2.0)
|
version: 4.36.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: 2.0.0-alpha.8
|
specifier: 2.0.0-alpha.8
|
||||||
@ -194,9 +191,6 @@ dependencies:
|
|||||||
react-string-replace:
|
react-string-replace:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
react-xarrows:
|
|
||||||
specifier: ^2.0.2
|
|
||||||
version: 2.0.2(react@18.2.0)
|
|
||||||
reactflow:
|
reactflow:
|
||||||
specifier: ^11.9.4
|
specifier: ^11.9.4
|
||||||
version: 11.9.4(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0)
|
version: 11.9.4(@types/react@18.2.29)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -865,10 +859,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
|
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@formkit/auto-animate@0.8.0:
|
|
||||||
resolution: {integrity: sha512-G8f7489ka0mWyi+1IEZT+xgIwcpWtRMmE2x+IrVoQ+KM1cP6VDj/TbujZjwxdb0P8w8b16/qBfViRmydbYHwMw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@getalby/sdk@2.5.0:
|
/@getalby/sdk@2.5.0:
|
||||||
resolution: {integrity: sha512-MRLgI6WxCCLgrar+qDqm/UhKs+V6yXzNm4y1bJRAuN72nkKT+TjTJHCmk9GjTngR3FrOfLbeMsPwBxCmbvfrLQ==}
|
resolution: {integrity: sha512-MRLgI6WxCCLgrar+qDqm/UhKs+V6yXzNm4y1bJRAuN72nkKT+TjTJHCmk9GjTngR3FrOfLbeMsPwBxCmbvfrLQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@ -1003,24 +993,17 @@ packages:
|
|||||||
- typescript
|
- typescript
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@nostr-fetch/adapter-ndk@0.12.2(@nostr-dev-kit/ndk@2.0.3)(nostr-fetch@0.13.0):
|
/@nostr-fetch/adapter-ndk@0.13.0(@nostr-dev-kit/ndk@2.0.3)(nostr-fetch@0.13.0):
|
||||||
resolution: {integrity: sha512-+7EVuxS5DDZvNo6qbfFp7xRHwIyjyi36hYkiQFDjbQ4gX5LKo9RIPB1P+1XGkOSDFshypTbovZCaFunscJ/zhQ==}
|
resolution: {integrity: sha512-pTpAmwdaDUymdaVbYDoBV0A+LvOj6VIQqhrcfsWlr2+O4vbOy5DLqE3189xFZhkW0I+eT9m18H9YxbHUZS85hw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nostr-dev-kit/ndk': ^0.7.5
|
'@nostr-dev-kit/ndk': ^0.8.4
|
||||||
nostr-fetch: ^0.12.2
|
nostr-fetch: ^0.13.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nostr-dev-kit/ndk': 2.0.3(typescript@5.2.2)
|
'@nostr-dev-kit/ndk': 2.0.3(typescript@5.2.2)
|
||||||
'@nostr-fetch/kernel': 0.12.2
|
'@nostr-fetch/kernel': 0.13.0
|
||||||
nostr-fetch: 0.13.0
|
nostr-fetch: 0.13.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@nostr-fetch/kernel@0.12.2:
|
|
||||||
resolution: {integrity: sha512-ja7StOV33NmdtAMGfQIS0/R0dAkLRm3QxN6u/YAQdp5mXER4BYxiQKxUS/dCoTCSX986MH2zp9Fm0f76u4VaNQ==}
|
|
||||||
dependencies:
|
|
||||||
'@noble/curves': 1.2.0
|
|
||||||
'@noble/hashes': 1.3.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@nostr-fetch/kernel@0.13.0:
|
/@nostr-fetch/kernel@0.13.0:
|
||||||
resolution: {integrity: sha512-KoF6pCezZvGP9ZQo23Ib4UDwJRd08xqVeVTQ44Z/b+gxBBrZCR0lnwOPS6KYBvdOfduynQEryNt5if5ZLxpRlg==}
|
resolution: {integrity: sha512-KoF6pCezZvGP9ZQo23Ib4UDwJRd08xqVeVTQ44Z/b+gxBBrZCR0lnwOPS6KYBvdOfduynQEryNt5if5ZLxpRlg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -4832,6 +4815,7 @@ packages:
|
|||||||
|
|
||||||
/lodash@4.17.21:
|
/lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/log-update@5.0.1:
|
/log-update@5.0.1:
|
||||||
resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==}
|
resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==}
|
||||||
@ -6094,17 +6078,6 @@ packages:
|
|||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-xarrows@2.0.2(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-tDlAqaxHNmy0vegW/6NdhoWyXJq1LANX/WUAlHyzoHe9BwFVnJPPDghmDjYeVr7XWFmBrVTUrHsrW7GKYI6HtQ==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8.0'
|
|
||||||
dependencies:
|
|
||||||
'@types/prop-types': 15.7.9
|
|
||||||
lodash: 4.17.21
|
|
||||||
prop-types: 15.8.1
|
|
||||||
react: 18.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/react@18.2.0:
|
/react@18.2.0:
|
||||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"width": 1080,
|
"width": 1080,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 1080,
|
"minWidth": 560,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"width": 1080,
|
"width": 1080,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 1080,
|
"minWidth": 560,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{
|
{
|
||||||
"width": 1080,
|
"width": 1080,
|
||||||
"height": 800,
|
"height": 800,
|
||||||
"minWidth": 1080,
|
"minWidth": 560,
|
||||||
"minHeight": 800,
|
"minHeight": 800,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "Lume",
|
"title": "Lume",
|
||||||
|
@ -52,6 +52,8 @@ export function CreateAccountScreen() {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
display_name: data.name,
|
display_name: data.name,
|
||||||
bio: data.about,
|
bio: data.about,
|
||||||
|
picture: picture,
|
||||||
|
avatar: picture,
|
||||||
};
|
};
|
||||||
|
|
||||||
const userPrivkey = generatePrivateKey();
|
const userPrivkey = generatePrivateKey();
|
||||||
@ -105,7 +107,7 @@ export function CreateAccountScreen() {
|
|||||||
if (filePath) {
|
if (filePath) {
|
||||||
await writeTextFile(
|
await writeTextFile(
|
||||||
filePath,
|
filePath,
|
||||||
`Generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`
|
`Nostr account, generated by Lume (lume.nu)\nPublic key: ${keys.npub}\nPrivate key: ${keys.nsec}`
|
||||||
);
|
);
|
||||||
|
|
||||||
setDownloaded(true);
|
setDownloaded(true);
|
||||||
|
@ -1,150 +0,0 @@
|
|||||||
import { webln } from '@getalby/sdk';
|
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
|
||||||
import { message } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AlbyIcon,
|
|
||||||
ArrowRightCircleIcon,
|
|
||||||
CancelIcon,
|
|
||||||
CheckCircleIcon,
|
|
||||||
LoaderIcon,
|
|
||||||
} from '@shared/icons';
|
|
||||||
|
|
||||||
export function NWCAlby() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isLoading, setIsloading] = useState(false);
|
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
|
||||||
|
|
||||||
const initAlby = async () => {
|
|
||||||
try {
|
|
||||||
setIsloading(true);
|
|
||||||
|
|
||||||
const provider = webln.NostrWebLNProvider.withNewSecret();
|
|
||||||
const walletConnectURL = provider.getNostrWalletConnectUrl(true);
|
|
||||||
|
|
||||||
// get auth url
|
|
||||||
const authURL = provider.getAuthorizationUrl({ name: 'Lume' });
|
|
||||||
|
|
||||||
// open auth window
|
|
||||||
/*
|
|
||||||
const webview = new WebviewWindow('alby', {
|
|
||||||
title: 'Connect Alby',
|
|
||||||
url: authURL.href,
|
|
||||||
center: true,
|
|
||||||
width: 400,
|
|
||||||
height: 650,
|
|
||||||
});
|
|
||||||
|
|
||||||
webview.listen('tauri://close-requested', async () => {
|
|
||||||
await db.secureSave('nwc', walletConnectURL);
|
|
||||||
setIsConnected(true);
|
|
||||||
setIsloading(false);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
} catch (e) {
|
|
||||||
setIsloading(false);
|
|
||||||
await message(e.toString(), { title: 'Connect Alby', type: 'error' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="inline-flex items-center gap-2.5">
|
|
||||||
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-neutral-200">
|
|
||||||
<AlbyIcon className="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">Alby</h5>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
Require alby account
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-9 w-min items-center justify-center rounded-lg bg-neutral-300 px-3 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
</div>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
|
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-neutral-400 dark:bg-neutral-600">
|
|
||||||
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
|
||||||
Alby integration (Beta)
|
|
||||||
</Dialog.Title>
|
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
|
||||||
<CancelIcon className="h-4 w-4 text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</Dialog.Close>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3 px-5 py-5">
|
|
||||||
<div className="relative flex h-40 items-center justify-center gap-4">
|
|
||||||
<div className="inline-flex h-16 w-16 items-end justify-center rounded-lg bg-black pb-2">
|
|
||||||
<img src="/lume.png" className="w-1/3" alt="Lume Logo" />
|
|
||||||
</div>
|
|
||||||
<div className="w-20 border border-dashed border-white/5" />
|
|
||||||
<div className="inline-flex h-16 w-16 items-center justify-center rounded-lg bg-white">
|
|
||||||
<AlbyIcon className="h-8 w-8" />
|
|
||||||
</div>
|
|
||||||
{isConnected ? (
|
|
||||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
|
|
||||||
<CheckCircleIcon className="h-5 w-5 text-green-500" />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
When you click "Connect", a new window will open and you need
|
|
||||||
to click the "Connect Wallet" button to grant Lume permission
|
|
||||||
to integrate with your Alby account.
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
All information will be encrypted and stored on the local machine.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => initAlby()}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<span className="w-5" />
|
|
||||||
<span>Connecting...</span>
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
|
||||||
</>
|
|
||||||
) : isConnected ? (
|
|
||||||
<>
|
|
||||||
<span className="w-5" />
|
|
||||||
<span>Connected</span>
|
|
||||||
<CheckCircleIcon className="h-5 w-5" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="w-5" />
|
|
||||||
<span>Connect</span>
|
|
||||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
66
src/app/nwc/components/form.tsx
Normal file
66
src/app/nwc/components/form.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
|
export function NWCForm({ setWalletConnectURL }) {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
const [uri, setUri] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (!uri.startsWith('nostr+walletconnect:')) {
|
||||||
|
toast.error(
|
||||||
|
'Connect URI is required and must start with format nostr+walletconnect:, please check again'
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uriObj = new URL(uri);
|
||||||
|
const params = new URLSearchParams(uriObj.search);
|
||||||
|
|
||||||
|
if (params.has('relay') && params.has('secret')) {
|
||||||
|
await db.secureSave(`${db.account.pubkey}-nwc`, uri);
|
||||||
|
setWalletConnectURL(uri);
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error('Connect URI is not valid, please check again');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<textarea
|
||||||
|
name="walletConnectURL"
|
||||||
|
value={uri}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
|
autoFocus={false}
|
||||||
|
onChange={(e) => setUri(e.target.value)}
|
||||||
|
placeholder="nostr+walletconnect://"
|
||||||
|
className="h-40 w-full resize-none rounded-lg bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={submit}
|
||||||
|
className="inline-flex h-9 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
{loading ? <LoaderIcon className="h-4 w-4 animate-spin" /> : 'Connect'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,165 +0,0 @@
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { Resolver, useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { ArrowRightCircleIcon, CancelIcon, LoaderIcon, WorldIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
uri: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolver: Resolver<FormValues> = async (values) => {
|
|
||||||
return {
|
|
||||||
values: values.uri ? values : {},
|
|
||||||
errors: !values.uri
|
|
||||||
? {
|
|
||||||
uri: {
|
|
||||||
type: 'required',
|
|
||||||
message: 'This is required.',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NWCOther() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
setError,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isDirty, isValid },
|
|
||||||
} = useForm<FormValues>({ resolver });
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isLoading, setIsloading] = useState(false);
|
|
||||||
|
|
||||||
const onSubmit = async (data: { [x: string]: string }) => {
|
|
||||||
try {
|
|
||||||
if (!data.uri.startsWith('nostr+walletconnect:')) {
|
|
||||||
setError('uri', {
|
|
||||||
type: 'custom',
|
|
||||||
message:
|
|
||||||
'Connect URI is required and must start with format nostr+walletconnect:, please check again',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsloading(true);
|
|
||||||
|
|
||||||
const uriObj = new URL(data.uri);
|
|
||||||
const params = new URLSearchParams(uriObj.search);
|
|
||||||
|
|
||||||
if (params.has('relay') && params.has('secret')) {
|
|
||||||
await db.secureSave('nwc', data.uri);
|
|
||||||
setIsloading(false);
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setIsloading(false);
|
|
||||||
setError('uri', {
|
|
||||||
type: 'custom',
|
|
||||||
message:
|
|
||||||
'Connect URI is required and must start with format nostr+walletconnect:, please check again',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<div className="flex items-center justify-between pt-4">
|
|
||||||
<div className="inline-flex items-center gap-2.5">
|
|
||||||
<div className="inline-flex h-11 w-11 items-center justify-center rounded-lg bg-neutral-200 dark:bg-neutral-700">
|
|
||||||
<WorldIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
|
||||||
URI String
|
|
||||||
</h5>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
Using format nostr+walletconnect:
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Dialog.Trigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-9 w-min items-center justify-center rounded-lg bg-neutral-300 px-3 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-700"
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
</Dialog.Trigger>
|
|
||||||
</div>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-white dark:bg-black" />
|
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-neutral-400 dark:bg-neutral-600">
|
|
||||||
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
|
||||||
Nostr Wallet Connect
|
|
||||||
</Dialog.Title>
|
|
||||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
|
|
||||||
<CancelIcon className="h-4 w-4 text-neutral-600 dark:text-neutral-400" />
|
|
||||||
</Dialog.Close>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="mb-0 flex flex-col gap-3 px-5 py-5"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="uri"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider text-neutral-600 dark:text-neutral-400"
|
|
||||||
>
|
|
||||||
Connect URI
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
{...register('uri', { required: true })}
|
|
||||||
placeholder="nostr+walletconnect:"
|
|
||||||
spellCheck={false}
|
|
||||||
autoComplete="off"
|
|
||||||
autoCorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-neutral-600 dark:text-neutral-400"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-red-400">
|
|
||||||
{errors.uri && <p>{errors.uri.message}</p>}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 text-center">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-blue-500 px-6 font-medium leading-none text-white hover:bg-blue-600 focus:outline-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<span className="w-5" />
|
|
||||||
<span>Connecting...</span>
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="w-5" />
|
|
||||||
<span>Connect</span>
|
|
||||||
<ArrowRightCircleIcon className="h-5 w-5" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<span className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
All information will be encrypted and stored on the local machine.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { NWCAlby } from '@app/nwc/components/alby';
|
import { NWCForm } from '@app/nwc/components/form';
|
||||||
import { NWCOther } from '@app/nwc/components/other';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@ -18,10 +17,9 @@ export function NWCScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getNWC() {
|
async function getNWC() {
|
||||||
const nwc = await db.secureLoad('nwc');
|
const nwc = await db.secureLoad(`${db.account.pubkey}-nwc`);
|
||||||
if (nwc) setWalletConnectURL(nwc);
|
if (nwc) setWalletConnectURL(nwc);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNWC();
|
getNWC();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -29,24 +27,19 @@ export function NWCScreen() {
|
|||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="flex w-full flex-col gap-5">
|
<div className="flex w-full flex-col gap-5">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-2xl font-bold leading-tight">
|
<h3 className="text-2xl font-bold leading-tight">Nostr Wallet Connect</h3>
|
||||||
Nostr Wallet Connect (Beta)
|
|
||||||
</h3>
|
|
||||||
<p className="leading-tight text-neutral-600 dark:text-neutral-400">
|
<p className="leading-tight text-neutral-600 dark:text-neutral-400">
|
||||||
Sending tips easily via Bitcoin Lightning.
|
Sending zap easily via Bitcoin Lightning.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto max-w-lg">
|
<div className="mx-auto max-w-lg">
|
||||||
{!walletConnectURL ? (
|
{!walletConnectURL ? (
|
||||||
<div className="flex w-full flex-col gap-4 divide-y divide-neutral-200 rounded-xl bg-neutral-100 p-3 dark:divide-neutral-800 dark:bg-neutral-900">
|
<NWCForm setWalletConnectURL={setWalletConnectURL} />
|
||||||
<NWCAlby />
|
|
||||||
<NWCOther />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="flex w-full flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<div className="mb-1 inline-flex items-center gap-1.5 text-sm text-teal-500">
|
<div className="flex items-center justify-center gap-1.5 text-sm text-teal-500">
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
<CheckCircleIcon className="h-4 w-4" />
|
||||||
<p>You're using nostr wallet connect</p>
|
<div>You're using nostr wallet connect</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<textarea
|
<textarea
|
||||||
@ -57,7 +50,7 @@ export function NWCScreen() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove()}
|
onClick={() => remove()}
|
||||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||||
>
|
>
|
||||||
Remove connection
|
Remove connection
|
||||||
</button>
|
</button>
|
||||||
@ -76,7 +69,7 @@ export function NWCScreen() {
|
|||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
To learn more about the details have a look at{' '}
|
To learn more about the details have a look at{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/getAlby/nips/blob/7-wallet-connect-patch/47.md"
|
href="https://github.com/nostr-protocol/nips/blob/master/47.md"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-blue-500"
|
className="text-blue-500"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
@ -87,13 +80,10 @@ export function NWCScreen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
About tipping
|
About zapping
|
||||||
</h5>
|
</h5>
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
Also known as Zap in other Nostr client.
|
Lume doesn't take any commission or platform fees when you zap
|
||||||
</p>
|
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
Lume doesn't take any commission or platform fees when you tip
|
|
||||||
someone.
|
someone.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { message, open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
||||||
import { fetch } from '@tauri-apps/plugin-http';
|
import { fetch } from '@tauri-apps/plugin-http';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { AvatarUploader } from '@shared/avatarUploader';
|
import {
|
||||||
import { BannerUploader } from '@shared/bannerUploader';
|
CancelIcon,
|
||||||
import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
|
CheckCircleIcon,
|
||||||
import { Image } from '@shared/image';
|
LoaderIcon,
|
||||||
|
PlusIcon,
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
UnverifiedIcon,
|
||||||
|
} from '@shared/icons';
|
||||||
|
|
||||||
interface NIP05 {
|
interface NIP05 {
|
||||||
names: {
|
names: {
|
||||||
@ -25,12 +29,12 @@ export function EditProfileModal() {
|
|||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
const [picture, setPicture] = useState('');
|
||||||
const [banner, setBanner] = useState(null);
|
const [banner, setBanner] = useState('');
|
||||||
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
||||||
|
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { publish } = useNostr();
|
const { ndk } = useNDK();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -75,12 +79,106 @@ export function EditProfileModal() {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadAvatar = async () => {
|
||||||
|
try {
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await readBinaryFile(selected.path);
|
||||||
|
const blob = new Blob([file]);
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('fileToUpload', blob);
|
||||||
|
data.append('submit', 'Upload Image');
|
||||||
|
|
||||||
|
const res = await fetch('https://nostr.build/api/v2/upload/files', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
const content = json.data[0];
|
||||||
|
|
||||||
|
setPicture(content.url);
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadBanner = async () => {
|
||||||
|
try {
|
||||||
|
// start loading
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await readBinaryFile(selected.path);
|
||||||
|
const blob = new Blob([file]);
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('fileToUpload', blob);
|
||||||
|
data.append('submit', 'Upload Image');
|
||||||
|
|
||||||
|
const res = await fetch('https://nostr.build/api/v2/upload/files', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
const content = json.data[0];
|
||||||
|
|
||||||
|
setBanner(content.url);
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NDKUserProfile) => {
|
const onSubmit = async (data: NDKUserProfile) => {
|
||||||
// start loading
|
// start loading
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
let event: NDKEvent;
|
|
||||||
|
|
||||||
const content = {
|
const content = {
|
||||||
...data,
|
...data,
|
||||||
username: data.name,
|
username: data.name,
|
||||||
@ -89,14 +187,14 @@ export function EditProfileModal() {
|
|||||||
image: data.picture,
|
image: data.picture,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const event = new NDKEvent(ndk);
|
||||||
|
event.kind = NDKKind.Metadata;
|
||||||
|
event.tags = [];
|
||||||
|
|
||||||
if (data.nip05) {
|
if (data.nip05) {
|
||||||
const nip05IsVerified = await verifyNIP05(data.nip05);
|
const nip05IsVerified = await verifyNIP05(data.nip05);
|
||||||
if (nip05IsVerified) {
|
if (nip05IsVerified) {
|
||||||
event = await publish({
|
event.content = JSON.stringify({ ...content, nip05: data.nip05 });
|
||||||
content: JSON.stringify({ ...content, nip05: data.nip05 }),
|
|
||||||
kind: 0,
|
|
||||||
tags: [],
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
setNIP05((prev) => ({ ...prev, verified: false }));
|
||||||
setError('nip05', {
|
setError('nip05', {
|
||||||
@ -105,14 +203,12 @@ export function EditProfileModal() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
event = await publish({
|
event.content = JSON.stringify(content);
|
||||||
content: JSON.stringify(content),
|
|
||||||
kind: 0,
|
|
||||||
tags: [],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.id) {
|
const publishedRelays = await event.publish();
|
||||||
|
|
||||||
|
if (publishedRelays) {
|
||||||
// invalid cache
|
// invalid cache
|
||||||
queryClient.invalidateQueries(['user', db.account.pubkey]);
|
queryClient.invalidateQueries(['user', db.account.pubkey]);
|
||||||
// reset form
|
// reset form
|
||||||
@ -144,7 +240,7 @@ export function EditProfileModal() {
|
|||||||
</button>
|
</button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-white dark:bg-black" />
|
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
||||||
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-xl rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
<div className="relative h-min w-full max-w-xl rounded-xl bg-neutral-100 dark:bg-neutral-900">
|
||||||
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-neutral-200 px-5 py-5 dark:border-neutral-800">
|
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-neutral-200 px-5 py-5 dark:border-neutral-800">
|
||||||
@ -173,18 +269,30 @@ export function EditProfileModal() {
|
|||||||
<div className="h-full w-full bg-black dark:bg-white" />
|
<div className="h-full w-full bg-black dark:bg-white" />
|
||||||
)}
|
)}
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<BannerUploader setBanner={setBanner} />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadBanner()}
|
||||||
|
className="inline-flex h-full w-full items-center justify-center bg-black/50"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-5 px-4">
|
<div className="mb-5 px-4">
|
||||||
<div className="relative z-10 -mt-7 h-14 w-14">
|
<div className="relative z-10 -mt-7 h-14 w-14 overflow-hidden rounded-xl ring-2 ring-neutral-900">
|
||||||
<Image
|
<img
|
||||||
src={picture}
|
src={picture}
|
||||||
alt="user's avatar"
|
alt="user's avatar"
|
||||||
className="h-14 w-14 rounded-lg object-cover ring-2 ring-neutral-900"
|
className="h-14 w-14 rounded-xl object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||||
<AvatarUploader setPicture={setPicture} />
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => uploadAvatar()}
|
||||||
|
className="inline-flex h-full w-full items-center justify-center rounded-xl bg-black/50"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,15 +50,15 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="h-56 w-full">
|
<div className="h-56 w-full overflow-hidden rounded-tl-lg">
|
||||||
{user.banner ? (
|
{user.banner ? (
|
||||||
<img
|
<img
|
||||||
src={user.banner}
|
src={user.banner}
|
||||||
alt="user banner"
|
alt="user banner"
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full rounded-tl-lg object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full bg-neutral-100 dark:bg-neutral-900" />
|
<div className="h-full w-full rounded-tl-lg bg-neutral-100 dark:bg-neutral-900" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="-mt-7 flex w-full flex-col items-center px-5">
|
<div className="-mt-7 flex w-full flex-col items-center px-5">
|
||||||
|
455
src/libs/ndk/cache.ts
Normal file
455
src/libs/ndk/cache.ts
Normal file
@ -0,0 +1,455 @@
|
|||||||
|
import { NDKEvent, NDKRelay, profileFromEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import type {
|
||||||
|
Hexpubkey,
|
||||||
|
NDKCacheAdapter,
|
||||||
|
NDKFilter,
|
||||||
|
NDKSubscription,
|
||||||
|
NDKUserProfile,
|
||||||
|
} from '@nostr-dev-kit/ndk';
|
||||||
|
import _debug from 'debug';
|
||||||
|
import { matchFilter } from 'nostr-tools';
|
||||||
|
import { LRUCache } from 'typescript-lru-cache';
|
||||||
|
|
||||||
|
import { createDatabase, db } from './db';
|
||||||
|
|
||||||
|
export { db } from './db';
|
||||||
|
|
||||||
|
interface NDKCacheAdapterDexieOptions {
|
||||||
|
/**
|
||||||
|
* The name of the database to use
|
||||||
|
*/
|
||||||
|
dbName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug instance to use for logging
|
||||||
|
*/
|
||||||
|
debug?: debug.IDebugger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of seconds to store events in Dexie (IndexedDB) before they expire
|
||||||
|
* Defaults to 3600 seconds (1 hour)
|
||||||
|
*/
|
||||||
|
expirationTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of profiles to keep in an LRU cache
|
||||||
|
*/
|
||||||
|
profileCacheSize?: number | 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class NDKCacheAdapterDexie implements NDKCacheAdapter {
|
||||||
|
public debug: debug.Debugger;
|
||||||
|
private expirationTime;
|
||||||
|
readonly locking;
|
||||||
|
public profiles?: LRUCache<Hexpubkey, NDKUserProfile>;
|
||||||
|
private dirtyProfiles: Set<Hexpubkey> = new Set();
|
||||||
|
|
||||||
|
constructor(opts: NDKCacheAdapterDexieOptions = {}) {
|
||||||
|
createDatabase(opts.dbName || 'ndk');
|
||||||
|
this.debug = opts.debug || _debug('ndk:dexie-adapter');
|
||||||
|
this.locking = true;
|
||||||
|
this.expirationTime = opts.expirationTime || 3600;
|
||||||
|
|
||||||
|
if (opts.profileCacheSize !== 'disabled') {
|
||||||
|
this.profiles = new LRUCache({
|
||||||
|
maxSize: opts.profileCacheSize || 100000,
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
this.dumpProfiles();
|
||||||
|
}, 1000 * 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async query(subscription: NDKSubscription): Promise<void> {
|
||||||
|
Promise.allSettled(
|
||||||
|
subscription.filters.map((filter) => this.processFilter(filter, subscription))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchProfile(pubkey: Hexpubkey) {
|
||||||
|
if (!this.profiles) return null;
|
||||||
|
|
||||||
|
let profile = this.profiles.get(pubkey);
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
const user = await db.users.get({ pubkey });
|
||||||
|
if (user) {
|
||||||
|
profile = user.profile;
|
||||||
|
this.profiles.set(pubkey, profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public saveProfile(pubkey: Hexpubkey, profile: NDKUserProfile) {
|
||||||
|
if (!this.profiles) return;
|
||||||
|
|
||||||
|
this.profiles.set(pubkey, profile);
|
||||||
|
|
||||||
|
this.dirtyProfiles.add(pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processFilter(
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<void> {
|
||||||
|
const _filter = { ...filter };
|
||||||
|
delete _filter.limit;
|
||||||
|
const filterKeys = Object.keys(_filter || {}).sort();
|
||||||
|
|
||||||
|
try {
|
||||||
|
(await this.byKindAndAuthor(filterKeys, filter, subscription)) ||
|
||||||
|
(await this.byAuthors(filterKeys, filter, subscription)) ||
|
||||||
|
(await this.byKinds(filterKeys, filter, subscription)) ||
|
||||||
|
(await this.byIdsQuery(filterKeys, filter, subscription)) ||
|
||||||
|
(await this.byNip33Query(filterKeys, filter, subscription)) ||
|
||||||
|
(await this.byTagsAndOptionallyKinds(filterKeys, filter, subscription));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setEvent(
|
||||||
|
event: NDKEvent,
|
||||||
|
_filter: NDKFilter,
|
||||||
|
relay?: NDKRelay
|
||||||
|
): Promise<void> {
|
||||||
|
if (event.kind === 0) {
|
||||||
|
if (!this.profiles) return;
|
||||||
|
|
||||||
|
const profile: NDKUserProfile = profileFromEvent(event);
|
||||||
|
this.profiles.set(event.pubkey, profile);
|
||||||
|
} else {
|
||||||
|
let addEvent = true;
|
||||||
|
|
||||||
|
if (event.isParamReplaceable()) {
|
||||||
|
const replaceableId = `${event.kind}:${event.pubkey}:${event.tagId()}`;
|
||||||
|
const existingEvent = await db.events.where({ id: replaceableId }).first();
|
||||||
|
if (
|
||||||
|
existingEvent &&
|
||||||
|
event.created_at &&
|
||||||
|
existingEvent.createdAt > event.created_at
|
||||||
|
) {
|
||||||
|
addEvent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addEvent) {
|
||||||
|
db.events.put({
|
||||||
|
id: event.tagId(),
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
content: event.content,
|
||||||
|
kind: event.kind!,
|
||||||
|
createdAt: event.created_at!,
|
||||||
|
relay: relay?.url,
|
||||||
|
event: JSON.stringify(event.rawEvent()),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't cache contact lists as tags since it's expensive
|
||||||
|
// and there is no use case for it
|
||||||
|
if (event.kind !== 3) {
|
||||||
|
event.tags.forEach((tag) => {
|
||||||
|
if (tag[0].length !== 1) return;
|
||||||
|
|
||||||
|
db.eventTags.put({
|
||||||
|
id: `${event.id}:${tag[0]}:${tag[1]}`,
|
||||||
|
eventId: event.id,
|
||||||
|
tag: tag[0],
|
||||||
|
value: tag[1],
|
||||||
|
tagValue: tag[0] + tag[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches by authors
|
||||||
|
*/
|
||||||
|
private async byAuthors(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<boolean> {
|
||||||
|
const f = ['authors'];
|
||||||
|
const hasAllKeys =
|
||||||
|
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||||
|
|
||||||
|
let foundEvents = false;
|
||||||
|
|
||||||
|
if (hasAllKeys && filter.authors) {
|
||||||
|
for (const pubkey of filter.authors) {
|
||||||
|
const events = await db.events.where({ pubkey }).toArray();
|
||||||
|
for (const event of events) {
|
||||||
|
let rawEvent;
|
||||||
|
try {
|
||||||
|
rawEvent = JSON.parse(event.event);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse event', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||||
|
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||||
|
subscription.eventReceived(ndkEvent, relay, true);
|
||||||
|
foundEvents = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches by kinds
|
||||||
|
*/
|
||||||
|
private async byKinds(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<boolean> {
|
||||||
|
const f = ['kinds'];
|
||||||
|
const hasAllKeys =
|
||||||
|
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||||
|
|
||||||
|
let foundEvents = false;
|
||||||
|
|
||||||
|
if (hasAllKeys && filter.kinds) {
|
||||||
|
for (const kind of filter.kinds) {
|
||||||
|
const events = await db.events.where({ kind }).toArray();
|
||||||
|
for (const event of events) {
|
||||||
|
let rawEvent;
|
||||||
|
try {
|
||||||
|
rawEvent = JSON.parse(event.event);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse event', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||||
|
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||||
|
subscription.eventReceived(ndkEvent, relay, true);
|
||||||
|
foundEvents = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches by ids
|
||||||
|
*/
|
||||||
|
private async byIdsQuery(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<boolean> {
|
||||||
|
const f = ['ids'];
|
||||||
|
const hasAllKeys =
|
||||||
|
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||||
|
|
||||||
|
if (hasAllKeys && filter.ids) {
|
||||||
|
for (const id of filter.ids) {
|
||||||
|
const event = await db.events.where({ id }).first();
|
||||||
|
if (!event) continue;
|
||||||
|
|
||||||
|
let rawEvent;
|
||||||
|
try {
|
||||||
|
rawEvent = JSON.parse(event.event);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse event', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||||
|
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||||
|
subscription.eventReceived(ndkEvent, relay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches by NIP-33
|
||||||
|
*/
|
||||||
|
private async byNip33Query(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<boolean> {
|
||||||
|
const f = ['#d', 'authors', 'kinds'];
|
||||||
|
const hasAllKeys =
|
||||||
|
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||||
|
|
||||||
|
if (hasAllKeys && filter.kinds && filter.authors) {
|
||||||
|
for (const kind of filter.kinds) {
|
||||||
|
const replaceableKind = kind >= 30000 && kind < 40000;
|
||||||
|
|
||||||
|
if (!replaceableKind) continue;
|
||||||
|
|
||||||
|
for (const author of filter.authors) {
|
||||||
|
for (const dTag of filter['#d']) {
|
||||||
|
const replaceableId = `${kind}:${author}:${dTag}`;
|
||||||
|
const event = await db.events.where({ id: replaceableId }).first();
|
||||||
|
if (!event) continue;
|
||||||
|
|
||||||
|
let rawEvent;
|
||||||
|
try {
|
||||||
|
rawEvent = JSON.parse(event.event);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse event', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||||
|
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||||
|
subscription.eventReceived(ndkEvent, relay, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches by kind & author
|
||||||
|
*/
|
||||||
|
private async byKindAndAuthor(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<boolean> {
|
||||||
|
const f = ['authors', 'kinds'];
|
||||||
|
const hasAllKeys =
|
||||||
|
filterKeys.length === f.length && f.every((k) => filterKeys.includes(k));
|
||||||
|
let foundEvents = false;
|
||||||
|
|
||||||
|
if (!hasAllKeys) return false;
|
||||||
|
|
||||||
|
if (filter.kinds && filter.authors) {
|
||||||
|
for (const kind of filter.kinds) {
|
||||||
|
for (const author of filter.authors) {
|
||||||
|
const events = await db.events.where({ kind, pubkey: author }).toArray();
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
let rawEvent;
|
||||||
|
try {
|
||||||
|
rawEvent = JSON.parse(event.event);
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse event', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||||
|
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||||
|
subscription.eventReceived(ndkEvent, relay, true);
|
||||||
|
foundEvents = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return foundEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches by tags and optionally filters by tags
|
||||||
|
*/
|
||||||
|
private async byTagsAndOptionallyKinds(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter,
|
||||||
|
subscription: NDKSubscription
|
||||||
|
): Promise<boolean> {
|
||||||
|
for (const filterKey of filterKeys) {
|
||||||
|
const isKind = filterKey === 'kinds';
|
||||||
|
const isTag = filterKey.startsWith('#') && filterKey.length === 2;
|
||||||
|
|
||||||
|
if (!isKind && !isTag) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await this.filterByTag(filterKeys, filter);
|
||||||
|
const kinds = filter.kinds as number[];
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
if (!kinds?.includes(event.kind!)) continue;
|
||||||
|
|
||||||
|
subscription.eventReceived(event, undefined, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async filterByTag(
|
||||||
|
filterKeys: string[],
|
||||||
|
filter: NDKFilter
|
||||||
|
): Promise<NDKEvent[]> {
|
||||||
|
const retEvents: NDKEvent[] = [];
|
||||||
|
|
||||||
|
for (const filterKey of filterKeys) {
|
||||||
|
if (filterKey.length !== 2) continue;
|
||||||
|
const tag = filterKey.slice(1);
|
||||||
|
// const values = filter[filterKey] as string[];
|
||||||
|
const values: string[] = [];
|
||||||
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
|
if (key === filterKey) values.push(value as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const value of values) {
|
||||||
|
const eventTags = await db.eventTags.where({ tagValue: tag + value }).toArray();
|
||||||
|
if (!eventTags.length) continue;
|
||||||
|
|
||||||
|
const eventIds = eventTags.map((t) => t.eventId);
|
||||||
|
|
||||||
|
const events = await db.events.where('id').anyOf(eventIds).toArray();
|
||||||
|
for (const event of events) {
|
||||||
|
let rawEvent;
|
||||||
|
try {
|
||||||
|
rawEvent = JSON.parse(event.event);
|
||||||
|
|
||||||
|
// Make sure all passed filters match the event
|
||||||
|
if (!matchFilter(filter, rawEvent)) continue;
|
||||||
|
} catch (e) {
|
||||||
|
console.log('failed to parse event', e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(undefined, rawEvent);
|
||||||
|
const relay = event.relay ? new NDKRelay(event.relay) : undefined;
|
||||||
|
ndkEvent.relay = relay;
|
||||||
|
retEvents.push(ndkEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async dumpProfiles(): Promise<void> {
|
||||||
|
const profiles = [];
|
||||||
|
|
||||||
|
if (!this.profiles) return;
|
||||||
|
|
||||||
|
for (const pubkey of this.dirtyProfiles) {
|
||||||
|
const profile = this.profiles.get(pubkey);
|
||||||
|
|
||||||
|
if (!profile) continue;
|
||||||
|
|
||||||
|
profiles.push({
|
||||||
|
pubkey,
|
||||||
|
profile,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profiles.length) {
|
||||||
|
await db.users.bulkPut(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dirtyProfiles.clear();
|
||||||
|
}
|
||||||
|
}
|
@ -55,7 +55,7 @@ export function ActiveAccount() {
|
|||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="aspect-square h-auto w-full animate-pulse rounded-lg bg-white/10" />
|
<div className="aspect-square h-auto w-full animate-pulse rounded-lg bg-neutral-300 dark:bg-neutral-700" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,31 +1,69 @@
|
|||||||
|
import { message, open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
|
||||||
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
|
||||||
|
|
||||||
export function AvatarUploader({
|
export function AvatarUploader({
|
||||||
setPicture,
|
setPicture,
|
||||||
}: {
|
}: {
|
||||||
setPicture: Dispatch<SetStateAction<string>>;
|
setPicture: Dispatch<SetStateAction<string>>;
|
||||||
}) {
|
}) {
|
||||||
const { upload } = useNostr();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadAvatar = async () => {
|
const uploadAvatar = async () => {
|
||||||
setLoading(true);
|
try {
|
||||||
const image = await upload(null);
|
// start loading
|
||||||
if (image.url) {
|
setLoading(true);
|
||||||
setPicture(image.url);
|
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await readBinaryFile(selected.path);
|
||||||
|
const blob = new Blob([file]);
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('fileToUpload', blob);
|
||||||
|
data.append('submit', 'Upload Image');
|
||||||
|
|
||||||
|
const res = await fetch('https://nostr.build/api/v2/upload/files', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
const content = json.data[0];
|
||||||
|
|
||||||
|
setPicture(content.url);
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => uploadAvatar()}
|
onClick={() => uploadAvatar()}
|
||||||
className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-100 px-1.5 py-1 text-sm font-medium text-blue-500 hover:border-blue-300 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-500 dark:hover:border-blue-800 dark:hover:bg-blue-800"
|
className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-100 px-1.5 py-1 text-sm font-medium text-blue-500 hover:border-blue-300 hover:bg-blue-200 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-500 dark:hover:border-blue-800 dark:hover:bg-blue-800"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
|
@ -1,24 +1,62 @@
|
|||||||
|
import { message, open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
|
||||||
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
import { LoaderIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
|
||||||
|
|
||||||
export function BannerUploader({
|
export function BannerUploader({
|
||||||
setBanner,
|
setBanner,
|
||||||
}: {
|
}: {
|
||||||
setBanner: Dispatch<SetStateAction<string>>;
|
setBanner: Dispatch<SetStateAction<string>>;
|
||||||
}) {
|
}) {
|
||||||
const { upload } = useNostr();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const uploadBanner = async () => {
|
const uploadBanner = async () => {
|
||||||
setLoading(true);
|
try {
|
||||||
const image = await upload(null);
|
// start loading
|
||||||
if (image.url) {
|
setLoading(true);
|
||||||
setBanner(image.url);
|
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image',
|
||||||
|
extensions: ['png', 'jpeg', 'jpg', 'gif'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await readBinaryFile(selected.path);
|
||||||
|
const blob = new Blob([file]);
|
||||||
|
|
||||||
|
const data = new FormData();
|
||||||
|
data.append('fileToUpload', blob);
|
||||||
|
data.append('submit', 'Upload Image');
|
||||||
|
|
||||||
|
const res = await fetch('https://nostr.build/api/v2/upload/files', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
const content = json.data[0];
|
||||||
|
|
||||||
|
setBanner(content.url);
|
||||||
|
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// stop loading
|
||||||
|
setLoading(false);
|
||||||
|
await message(`Upload failed, error: ${e}`, { title: 'Lume', type: 'error' });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -67,7 +67,7 @@ export function NoteRepost({ id, pubkey }: { id: string; pubkey: string }) {
|
|||||||
</Tooltip.Portal>
|
</Tooltip.Portal>
|
||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
<AlertDialog.Portal>
|
<AlertDialog.Portal>
|
||||||
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-white/20" />
|
<AlertDialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-black/20" />
|
||||||
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
<AlertDialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||||
<div className="relative h-min w-full max-w-md rounded-xl bg-white dark:bg-black">
|
<div className="relative h-min w-full max-w-md rounded-xl bg-white dark:bg-black">
|
||||||
<div className="flex flex-col gap-2 border-b border-neutral-100 px-5 py-6 dark:border-neutral-900">
|
<div className="flex flex-col gap-2 border-b border-neutral-100 px-5 py-6 dark:border-neutral-900">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@ -68,13 +67,6 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
<div className="mb-1 select-text rounded-lg bg-white/5 p-1.5 text-sm">
|
|
||||||
Lume cannot find this post with your current relays, but you can view it via
|
|
||||||
njump.me.{' '}
|
|
||||||
<Link to={noteLink} className="text-blue-500">
|
|
||||||
Learn more
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<LinkPreview urls={[noteLink]} />
|
<LinkPreview urls={[noteLink]} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -87,10 +79,10 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
onKeyDown={(e) => openThread(e, id)}
|
onKeyDown={(e) => openThread(e, id)}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="mt-3 cursor-default rounded-lg bg-neutral-200 px-3 py-3 dark:bg-neutral-800"
|
className="mt-3 cursor-default rounded-lg border border-neutral-300 bg-neutral-200 p-3 dark:border-neutral-700 dark:bg-neutral-800"
|
||||||
>
|
>
|
||||||
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
||||||
<div className="mt-1.5">{renderKind(data)}</div>
|
<div className="mt-1">{renderKind(data)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -57,6 +57,7 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
|
|||||||
<hr className="my-4 h-px w-full border-none bg-neutral-100" />
|
<hr className="my-4 h-px w-full border-none bg-neutral-100" />
|
||||||
<NoteReplyForm id={params.content} />
|
<NoteReplyForm id={params.content} />
|
||||||
<ReplyList id={params.content} />
|
<ReplyList id={params.content} />
|
||||||
|
<div className="h-10" />
|
||||||
</div>
|
</div>
|
||||||
</WidgetWrapper>
|
</WidgetWrapper>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user