mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
wip: clean up & refactor
This commit is contained in:
parent
c05bb54976
commit
ab61bfb2cd
@ -20,14 +20,12 @@
|
|||||||
"@ctrl/magnet-link": "^3.1.2",
|
"@ctrl/magnet-link": "^3.1.2",
|
||||||
"@headlessui/react": "^1.7.16",
|
"@headlessui/react": "^1.7.16",
|
||||||
"@nostr-dev-kit/ndk": "^0.8.17",
|
"@nostr-dev-kit/ndk": "^0.8.17",
|
||||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-collapsible": "^1.0.3",
|
"@radix-ui/react-collapsible": "^1.0.3",
|
||||||
"@radix-ui/react-dialog": "^1.0.4",
|
"@radix-ui/react-dialog": "^1.0.4",
|
||||||
"@radix-ui/react-popover": "^1.0.6",
|
"@radix-ui/react-popover": "^1.0.6",
|
||||||
"@radix-ui/react-tooltip": "^1.0.6",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tanstack/react-query": "^4.32.6",
|
"@tanstack/react-query": "^4.32.6",
|
||||||
"@tanstack/react-query-devtools": "^4.32.6",
|
|
||||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||||
"@tauri-apps/api": "2.0.0-alpha.6",
|
"@tauri-apps/api": "2.0.0-alpha.6",
|
||||||
"@tauri-apps/cli": "2.0.0-alpha.11",
|
"@tauri-apps/cli": "2.0.0-alpha.11",
|
||||||
@ -61,7 +59,6 @@
|
|||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
"lru-cache": "^10.0.1",
|
"lru-cache": "^10.0.1",
|
||||||
"nostr-fetch": "^0.12.2",
|
|
||||||
"nostr-tools": "^1.14.0",
|
"nostr-tools": "^1.14.0",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -10,9 +10,6 @@ dependencies:
|
|||||||
'@nostr-dev-kit/ndk':
|
'@nostr-dev-kit/ndk':
|
||||||
specifier: ^0.8.17
|
specifier: ^0.8.17
|
||||||
version: 0.8.17(typescript@5.1.6)
|
version: 0.8.17(typescript@5.1.6)
|
||||||
'@nostr-fetch/adapter-ndk':
|
|
||||||
specifier: ^0.12.2
|
|
||||||
version: 0.12.2(@nostr-dev-kit/ndk@0.8.17)(nostr-fetch@0.12.2)
|
|
||||||
'@radix-ui/react-alert-dialog':
|
'@radix-ui/react-alert-dialog':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -31,9 +28,6 @@ dependencies:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^4.32.6
|
specifier: ^4.32.6
|
||||||
version: 4.32.6(react-dom@18.2.0)(react@18.2.0)
|
version: 4.32.6(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@tanstack/react-query-devtools':
|
|
||||||
specifier: ^4.32.6
|
|
||||||
version: 4.32.6(@tanstack/react-query@4.32.6)(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: 3.0.0-beta.54
|
specifier: 3.0.0-beta.54
|
||||||
version: 3.0.0-beta.54(react@18.2.0)
|
version: 3.0.0-beta.54(react@18.2.0)
|
||||||
@ -133,9 +127,6 @@ dependencies:
|
|||||||
lru-cache:
|
lru-cache:
|
||||||
specifier: ^10.0.1
|
specifier: ^10.0.1
|
||||||
version: 10.0.1
|
version: 10.0.1
|
||||||
nostr-fetch:
|
|
||||||
specifier: ^0.12.2
|
|
||||||
version: 0.12.2
|
|
||||||
nostr-tools:
|
nostr-tools:
|
||||||
specifier: ^1.14.0
|
specifier: ^1.14.0
|
||||||
version: 1.14.0
|
version: 1.14.0
|
||||||
@ -1003,24 +994,6 @@ packages:
|
|||||||
- typescript
|
- typescript
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@nostr-fetch/adapter-ndk@0.12.2(@nostr-dev-kit/ndk@0.8.17)(nostr-fetch@0.12.2):
|
|
||||||
resolution: {integrity: sha512-+7EVuxS5DDZvNo6qbfFp7xRHwIyjyi36hYkiQFDjbQ4gX5LKo9RIPB1P+1XGkOSDFshypTbovZCaFunscJ/zhQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@nostr-dev-kit/ndk': ^0.7.5
|
|
||||||
nostr-fetch: ^0.12.2
|
|
||||||
dependencies:
|
|
||||||
'@nostr-dev-kit/ndk': 0.8.17(typescript@5.1.6)
|
|
||||||
'@nostr-fetch/kernel': 0.12.2
|
|
||||||
nostr-fetch: 0.12.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@nostr-fetch/kernel@0.12.2:
|
|
||||||
resolution: {integrity: sha512-ja7StOV33NmdtAMGfQIS0/R0dAkLRm3QxN6u/YAQdp5mXER4BYxiQKxUS/dCoTCSX986MH2zp9Fm0f76u4VaNQ==}
|
|
||||||
dependencies:
|
|
||||||
'@noble/curves': 1.1.0
|
|
||||||
'@noble/hashes': 1.3.1
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@popperjs/core@2.11.8:
|
/@popperjs/core@2.11.8:
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -1733,32 +1706,10 @@ packages:
|
|||||||
tailwindcss: 3.3.3
|
tailwindcss: 3.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@tanstack/match-sorter-utils@8.8.4:
|
|
||||||
resolution: {integrity: sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
dependencies:
|
|
||||||
remove-accents: 0.4.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@tanstack/query-core@4.32.6:
|
/@tanstack/query-core@4.32.6:
|
||||||
resolution: {integrity: sha512-YVB+mVWENQwPyv+40qO7flMgKZ0uI41Ph7qXC2Zf1ft5AIGfnXnMZyifB2ghhZ27u+5wm5mlzO4Y6lwwadzxCA==}
|
resolution: {integrity: sha512-YVB+mVWENQwPyv+40qO7flMgKZ0uI41Ph7qXC2Zf1ft5AIGfnXnMZyifB2ghhZ27u+5wm5mlzO4Y6lwwadzxCA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tanstack/react-query-devtools@4.32.6(@tanstack/react-query@4.32.6)(react-dom@18.2.0)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-Gd9pBkm2sbeze9P5Yp8R7y0rZVUdoIOhduomDjz138WdJuVbRS4Y8p6gX2uMJFsUFVe7jA6fX/D6NfQ9o5OS/A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tanstack/react-query': ^4.32.6
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
|
||||||
dependencies:
|
|
||||||
'@tanstack/match-sorter-utils': 8.8.4
|
|
||||||
'@tanstack/react-query': 4.32.6(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
superjson: 1.13.1
|
|
||||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@tanstack/react-query@4.32.6(react-dom@18.2.0)(react@18.2.0):
|
/@tanstack/react-query@4.32.6(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-AITu/IKJJJXsHHeXNBy5bclu12t08usMCY0vFC2dh9SP/w6JAk5U9GwfjOIPj3p+ATADZvxQPe8UiCtMLNeQbg==}
|
resolution: {integrity: sha512-AITu/IKJJJXsHHeXNBy5bclu12t08usMCY0vFC2dh9SP/w6JAk5U9GwfjOIPj3p+ATADZvxQPe8UiCtMLNeQbg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -2998,13 +2949,6 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/copy-anything@3.0.5:
|
|
||||||
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
|
||||||
engines: {node: '>=12.13'}
|
|
||||||
dependencies:
|
|
||||||
is-what: 4.1.15
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/create-esm-loader@0.2.3:
|
/create-esm-loader@0.2.3:
|
||||||
resolution: {integrity: sha512-cllzD6IU/mzXBs5OdQVWL3+ne5Elpu3Wdm7h5OldMbGXk76yr9XzHlQXWJ4zfs0ZAibe26rkbs4KvMAJm7fIZA==}
|
resolution: {integrity: sha512-cllzD6IU/mzXBs5OdQVWL3+ne5Elpu3Wdm7h5OldMbGXk76yr9XzHlQXWJ4zfs0ZAibe26rkbs4KvMAJm7fIZA==}
|
||||||
engines: {node: '>=14.x'}
|
engines: {node: '>=14.x'}
|
||||||
@ -4280,11 +4224,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.2
|
call-bind: 1.0.2
|
||||||
|
|
||||||
/is-what@4.1.15:
|
|
||||||
resolution: {integrity: sha512-uKua1wfy3Yt+YqsD6mTUEa2zSi3G1oPlqTflgaPJ7z63vUGN5pxFpnQfeSLMFnJDEsdvOtkp1rUWkYjB4YfhgA==}
|
|
||||||
engines: {node: '>=12.13'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/isarray@2.0.5:
|
/isarray@2.0.5:
|
||||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||||
|
|
||||||
@ -5124,12 +5063,6 @@ packages:
|
|||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/nostr-fetch@0.12.2:
|
|
||||||
resolution: {integrity: sha512-0WH0LlaPcIvG5gOIwrGtzRwHpaZ+JQxH0XG7EjQcKpviePVmVKWK7UAGuzuWJj/V0iSqnDGOLSQ+HSEBjGVCEQ==}
|
|
||||||
dependencies:
|
|
||||||
'@nostr-fetch/kernel': 0.12.2
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/nostr-tools@1.14.0:
|
/nostr-tools@1.14.0:
|
||||||
resolution: {integrity: sha512-hwq2i1z5/DneXRE5Zu/TzQuKzVLcB+gOdfT9CeoiScvNw/2dWRGJvyTXIdF92d7NQ7nMcEwqVJPDytLpEpiiKw==}
|
resolution: {integrity: sha512-hwq2i1z5/DneXRE5Zu/TzQuKzVLcB+gOdfT9CeoiScvNw/2dWRGJvyTXIdF92d7NQ7nMcEwqVJPDytLpEpiiKw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6016,10 +5949,6 @@ packages:
|
|||||||
unified: 10.1.2
|
unified: 10.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/remove-accents@0.4.2:
|
|
||||||
resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/resolve-from@4.0.0:
|
/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -6354,13 +6283,6 @@ packages:
|
|||||||
time-span: 5.1.0
|
time-span: 5.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/superjson@1.13.1:
|
|
||||||
resolution: {integrity: sha512-AVH2eknm9DEd3qvxM4Sq+LTCkSXE2ssfh1t11MHMXyYXFQyQ1HLgVvV+guLTsaQnJU3gnaVo34TohHPulY/wLg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
dependencies:
|
|
||||||
copy-anything: 3.0.5
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/supports-color@5.5.0:
|
/supports-color@5.5.0:
|
||||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
events (
|
events (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
cache_key TEXT NOT NULL UNIQUE,
|
cache_key TEXT NOT NULL,
|
||||||
event_id TEXT NOT NULL UNIQUE,
|
event_id TEXT NOT NULL UNIQUE,
|
||||||
event_kind INTEGER NOT NULL DEFAULT 1,
|
event_kind INTEGER NOT NULL DEFAULT 1,
|
||||||
event TEXT NOT NULL
|
event TEXT NOT NULL
|
||||||
|
8
src-tauri/migrations/20230816090508_clean_up_tables.sql
Normal file
8
src-tauri/migrations/20230816090508_clean_up_tables.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
DROP TABLE IF EXISTS notes;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS chats;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS metadata;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS replies;
|
@ -123,6 +123,12 @@ fn main() {
|
|||||||
sql: include_str!("../migrations/20230814083543_add_events_table.sql"),
|
sql: include_str!("../migrations/20230814083543_add_events_table.sql"),
|
||||||
kind: MigrationKind::Up,
|
kind: MigrationKind::Up,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 20230816090508,
|
||||||
|
description: "clean up tables",
|
||||||
|
sql: include_str!("../migrations/20230816090508_clean_up_tables.sql"),
|
||||||
|
kind: MigrationKind::Up,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.build(),
|
.build(),
|
||||||
@ -138,7 +144,6 @@ fn main() {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
// let salt = Alphanumeric.sample_string(&mut rand::thread_rng(), 12);
|
|
||||||
let key = argon2::hash_raw(
|
let key = argon2::hash_raw(
|
||||||
password.as_ref(),
|
password.as_ref(),
|
||||||
b"LUME_NEED_RUST_DEVELOPER_HELP_MAKE_SALT_RANDOM",
|
b"LUME_NEED_RUST_DEVELOPER_HELP_MAKE_SALT_RANDOM",
|
||||||
|
@ -42,9 +42,13 @@ export function CreateStep1Screen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const download = async () => {
|
const download = async () => {
|
||||||
await writeTextFile('lume-keys.txt', `Public key: ${npub}\nPrivate key: ${nsec}`, {
|
await writeTextFile(
|
||||||
|
`nostr_keys_${new Date().toISOString().slice(0, 10)}.txt`,
|
||||||
|
`Generated by Lume (lume.nu)\nPublic key: ${npub}\nPrivate key: ${nsec}`,
|
||||||
|
{
|
||||||
dir: BaseDirectory.Download,
|
dir: BaseDirectory.Download,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
setDownloaded(true);
|
setDownloaded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
|
|
||||||
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
import { ArrowRightCircleIcon, CheckCircleIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useOnboarding } from '@stores/onboarding';
|
import { useOnboarding } from '@stores/onboarding';
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
@ -52,7 +52,7 @@ export function OnboardStep2Screen() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
await db.createWidget(BLOCK_KINDS.hashtag, tag, tag.replace('#', ''));
|
await db.createWidget(widgetKinds.hashtag, tag, tag.replace('#', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate('/auth/onboarding/step-3', { replace: true });
|
navigate('/auth/onboarding/step-3', { replace: true });
|
||||||
|
@ -11,8 +11,6 @@ import { EyeOffIcon, EyeOnIcon, LoaderIcon } from '@shared/icons';
|
|||||||
|
|
||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
password: string;
|
password: string;
|
||||||
privkey: string;
|
privkey: string;
|
||||||
|
@ -60,18 +60,19 @@ export function UnlockScreen() {
|
|||||||
// redirect to home
|
// redirect to home
|
||||||
navigate('/', { replace: true });
|
navigate('/', { replace: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
setLoading(false);
|
||||||
setError('password', {
|
setError('password', {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
message: e,
|
message: e,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
setLoading(false);
|
||||||
setError('password', {
|
setError('password', {
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
message: 'Password is required and must be greater than 3',
|
message: 'Password is required and must be greater than 3',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -118,7 +119,7 @@ export function UnlockScreen() {
|
|||||||
<>
|
<>
|
||||||
<span className="w-5" />
|
<span className="w-5" />
|
||||||
<span>Decryting...</span>
|
<span>Decryting...</span>
|
||||||
<LoaderIcon className="h-5 w-5" />
|
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { LogicalSize, appWindow } from '@tauri-apps/plugin-window';
|
import { LogicalSize, getCurrent } from '@tauri-apps/plugin-window';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
import { ArrowRightCircleIcon } from '@shared/icons/arrowRightCircle';
|
||||||
|
|
||||||
export function WelcomeScreen() {
|
export function WelcomeScreen() {
|
||||||
|
const appWindow = getCurrent();
|
||||||
|
|
||||||
async function setWindow() {
|
async function setWindow() {
|
||||||
await appWindow.setSize(new LogicalSize(400, 500));
|
await appWindow.setSize(new LogicalSize(400, 500));
|
||||||
await appWindow.setResizable(false);
|
await appWindow.setResizable(false);
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import { Popover, Transition } from '@headlessui/react';
|
|
||||||
import { Fragment } from 'react';
|
|
||||||
|
|
||||||
import { MutedItem } from '@app/channel/components/mutedItem';
|
|
||||||
|
|
||||||
import { MuteIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
export function ChannelBlackList({ blacklist }: { blacklist: any }) {
|
|
||||||
return (
|
|
||||||
<Popover className="relative">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<Popover.Button
|
|
||||||
className={`group inline-flex h-8 w-8 items-center justify-center rounded-md ring-2 ring-zinc-950 focus:outline-none ${
|
|
||||||
open ? 'bg-zinc-800 hover:bg-zinc-700' : 'bg-zinc-900 hover:bg-zinc-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<MuteIcon
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className="text-white/50 group-hover:text-white"
|
|
||||||
/>
|
|
||||||
</Popover.Button>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute right-0 z-10 mt-1 w-screen max-w-xs transform px-4 sm:px-0">
|
|
||||||
<div className="shadow-popover flex flex-col gap-2 overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900">
|
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 p-3">
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
<h3 className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text font-semibold leading-none text-transparent">
|
|
||||||
Your muted list
|
|
||||||
</h3>
|
|
||||||
<p className="text-base leading-tight text-white/50">
|
|
||||||
Currently, unmute only affect locally, when you move to new client,
|
|
||||||
muted list will loaded again
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2 px-3 pb-3 pt-1">
|
|
||||||
{blacklist.map((item: any) => (
|
|
||||||
<MutedItem key={item.id} data={item} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react';
|
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { Fragment, useContext, useEffect, useState } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { createChannel } from '@libs/storage';
|
|
||||||
|
|
||||||
import { AvatarUploader } from '@shared/avatarUploader';
|
|
||||||
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
|
|
||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/date';
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function ChannelCreateModal() {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
|
||||||
|
|
||||||
const { account } = useAccount();
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
setValue,
|
|
||||||
formState: { isDirty, isValid },
|
|
||||||
} = useForm();
|
|
||||||
|
|
||||||
const addChannel = useMutation({
|
|
||||||
mutationFn: (event: any) => {
|
|
||||||
return createChannel(
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.name,
|
|
||||||
event.picture,
|
|
||||||
event.about,
|
|
||||||
event.created_at
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['channels'] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
|
||||||
ndk.signer = signer;
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
// build event
|
|
||||||
event.content = JSON.stringify(data);
|
|
||||||
event.kind = 40;
|
|
||||||
event.created_at = dateToUnix();
|
|
||||||
event.pubkey = account.pubkey;
|
|
||||||
event.tags = [];
|
|
||||||
|
|
||||||
// publish event
|
|
||||||
event.publish();
|
|
||||||
|
|
||||||
// insert to database
|
|
||||||
addChannel.mutate({
|
|
||||||
...event,
|
|
||||||
name: data.name,
|
|
||||||
picture: data.picture,
|
|
||||||
about: data.about,
|
|
||||||
});
|
|
||||||
|
|
||||||
// reset form
|
|
||||||
reset();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// close modal
|
|
||||||
setIsOpen(false);
|
|
||||||
// redirect to channel page
|
|
||||||
navigate(`/channel/${event.id}`);
|
|
||||||
}, 1000);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('error: ', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue('picture', image);
|
|
||||||
}, [setValue, image]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openModal()}
|
|
||||||
className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
|
||||||
<PlusIcon width={12} height={12} className="text-white/50" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="font-medium text-white/50">Create channel</h5>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
|
||||||
</Transition.Child>
|
|
||||||
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-lg border-t border-zinc-800/50 bg-zinc-900">
|
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-lg font-semibold leading-none text-white"
|
|
||||||
>
|
|
||||||
Create channel
|
|
||||||
</Dialog.Title>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Dialog.Description className="text-sm leading-tight text-white/50">
|
|
||||||
Channels are freedom square, everyone can speech freely, no one can
|
|
||||||
stop you or deceive what to speech
|
|
||||||
</Dialog.Description>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
className="mb-0 flex h-full w-full flex-col gap-4"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type={'hidden'}
|
|
||||||
{...register('picture')}
|
|
||||||
value={image}
|
|
||||||
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="text-sm font-medium uppercase tracking-wider text-white/50">
|
|
||||||
Picture
|
|
||||||
</span>
|
|
||||||
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
|
||||||
<Image
|
|
||||||
src={image}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt="channel picture"
|
|
||||||
className="relative z-10 h-11 w-11 rounded-md"
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-3 right-3 z-10">
|
|
||||||
<AvatarUploader setPicture={setImage} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
|
||||||
>
|
|
||||||
Channel name *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type={'text'}
|
|
||||||
{...register('name', {
|
|
||||||
required: true,
|
|
||||||
minLength: 4,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label
|
|
||||||
htmlFor="about"
|
|
||||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
{...register('about')}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-20 items-center justify-between gap-1 rounded-lg bg-zinc-800 px-4 py-2">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="font-semibold leading-none text-white">
|
|
||||||
Encrypted
|
|
||||||
</span>
|
|
||||||
<p className="w-4/5 text-sm leading-none text-white/50">
|
|
||||||
All messages are encrypted and only invited members can view and
|
|
||||||
send message
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled
|
|
||||||
className="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent bg-zinc-900 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-600 focus:ring-offset-2"
|
|
||||||
role="switch"
|
|
||||||
aria-checked="false"
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none inline-block h-5 w-5 translate-x-0 transform rounded-full bg-zinc-600 shadow ring-0 transition duration-200 ease-in-out" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isDirty || !isValid}
|
|
||||||
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
|
||||||
) : (
|
|
||||||
'Create channel →'
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { NavLink } from 'react-router-dom';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
|
|
||||||
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
|
|
||||||
|
|
||||||
export function ChannelsListItem({ data }: { data: any }) {
|
|
||||||
const channel = useChannelProfile(data.event_id);
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
to={`/channel/${data.event_id}`}
|
|
||||||
preventScrollReset={true}
|
|
||||||
className={({ isActive }) =>
|
|
||||||
twMerge(
|
|
||||||
'inline-flex h-9 items-center gap-2.5 rounded-md px-2.5',
|
|
||||||
isActive ? 'bg-zinc-900/50 text-white' : ''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
|
|
||||||
<span className="text-xs text-white">#</span>
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex w-full items-center justify-between">
|
|
||||||
<h5 className="truncate font-medium text-zinc-200">{channel?.name}</h5>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{data.new_messages && (
|
|
||||||
<span className="inline-flex w-8 items-center justify-center rounded bg-fuchsia-400/10 px-1 py-1 text-xs font-medium text-fuchsia-500 ring-1 ring-inset ring-fuchsia-400/20">
|
|
||||||
{data.new_messages}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { ChannelCreateModal } from '@app/channel/components/createModal';
|
|
||||||
import { ChannelsListItem } from '@app/channel/components/item';
|
|
||||||
|
|
||||||
import { getChannels } from '@libs/storage';
|
|
||||||
|
|
||||||
export function ChannelsList() {
|
|
||||||
const {
|
|
||||||
status,
|
|
||||||
data: channels,
|
|
||||||
isFetching,
|
|
||||||
} = useQuery(
|
|
||||||
['channels'],
|
|
||||||
async () => {
|
|
||||||
return await getChannels();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
{status === 'loading' ? (
|
|
||||||
<>
|
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
|
||||||
<div className="h-3.5 w-full animate-pulse rounded-sm bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
|
||||||
<div className="h-3.5 w-full animate-pulse rounded-sm bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
channels.map((item: { event_id: string }) => (
|
|
||||||
<ChannelsListItem key={item.event_id} data={item} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{isFetching && (
|
|
||||||
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
|
|
||||||
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
|
|
||||||
<div className="h-3.5 w-full animate-pulse rounded-sm bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ChannelCreateModal />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
|
||||||
|
|
||||||
export function Member({ pubkey }: { pubkey: string }) {
|
|
||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isError || isLoading ? (
|
|
||||||
<div className="h-7 w-7 animate-pulse rounded bg-zinc-800" />
|
|
||||||
) : (
|
|
||||||
<Image
|
|
||||||
className="inline-block h-7 w-7 rounded"
|
|
||||||
src={user?.image}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { Member } from '@app/channel/components/member';
|
|
||||||
|
|
||||||
import { getChannelUsers } from '@libs/storage';
|
|
||||||
|
|
||||||
export function ChannelMembers({ id }: { id: string }) {
|
|
||||||
const { status, data, isFetching } = useQuery(['channel-members', id], async () => {
|
|
||||||
return await getChannelUsers(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-3">
|
|
||||||
<h5 className="border-b border-zinc-900 pb-1 font-semibold text-zinc-200">
|
|
||||||
Members
|
|
||||||
</h5>
|
|
||||||
<div className="mt-3 flex w-full flex-wrap gap-1.5">
|
|
||||||
{status === 'loading' || isFetching ? (
|
|
||||||
<p>Loading...</p>
|
|
||||||
) : (
|
|
||||||
data.map((member: { pubkey: string }) => (
|
|
||||||
<Member key={member.pubkey} pubkey={member.pubkey} />
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useContext, useState } from 'react';
|
|
||||||
|
|
||||||
import { UserReply } from '@app/channel/components/messages/userReply';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
|
|
||||||
import { CancelIcon, EnterIcon } from '@shared/icons';
|
|
||||||
import { MediaUploader } from '@shared/mediaUploader';
|
|
||||||
|
|
||||||
import { useChannelMessages } from '@stores/channels';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/date';
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function ChannelMessageForm({ channelID }: { channelID: string }) {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
|
||||||
const [replyTo, closeReply] = useChannelMessages((state: any) => [
|
|
||||||
state.replyTo,
|
|
||||||
state.closeReply,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { account } = useAccount();
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
let tags: string[][];
|
|
||||||
|
|
||||||
if (replyTo.id !== null) {
|
|
||||||
tags = [
|
|
||||||
['e', channelID, '', 'root'],
|
|
||||||
['e', replyTo.id, '', 'reply'],
|
|
||||||
['p', replyTo.pubkey, ''],
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
tags = [['e', channelID, '', 'root']];
|
|
||||||
}
|
|
||||||
|
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
|
||||||
ndk.signer = signer;
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
// build event
|
|
||||||
event.content = value;
|
|
||||||
event.kind = 42;
|
|
||||||
event.created_at = dateToUnix();
|
|
||||||
event.pubkey = account.pubkey;
|
|
||||||
event.tags = tags;
|
|
||||||
|
|
||||||
// publish event
|
|
||||||
event.publish();
|
|
||||||
|
|
||||||
// reset state
|
|
||||||
setValue('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnterPress = (e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
submit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopReply = () => {
|
|
||||||
closeReply();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`relative w-full ${replyTo.id ? 'h-36' : 'h-24'}`}>
|
|
||||||
{replyTo.id && (
|
|
||||||
<div className="absolute left-0 top-0 z-10 h-16 w-full p-[2px]">
|
|
||||||
<div className="flex h-full w-full items-center justify-between rounded-t-md border-b border-zinc-700/70 bg-zinc-900 px-3">
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<UserReply pubkey={replyTo.pubkey} />
|
|
||||||
<div className="-mt-5 pl-[38px]">
|
|
||||||
<div className="text-base text-white">{replyTo.content}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => stopReply()}
|
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
|
|
||||||
>
|
|
||||||
<CancelIcon width={12} height={12} className="text-white" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
onKeyDown={handleEnterPress}
|
|
||||||
spellCheck={false}
|
|
||||||
placeholder="Message"
|
|
||||||
className={`relative ${
|
|
||||||
replyTo.id ? 'h-36 pt-16' : 'h-24 pt-3'
|
|
||||||
} w-full resize-none rounded-md bg-zinc-800 px-5 !outline-none placeholder:text-white/50`}
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-0 right-2 h-11">
|
|
||||||
<div className="flex h-full items-center justify-end gap-3 text-white/50">
|
|
||||||
<MediaUploader setState={setValue} />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={submit}
|
|
||||||
className="inline-flex items-center gap-1 text-sm leading-none"
|
|
||||||
>
|
|
||||||
<EnterIcon width={14} height={14} className="" />
|
|
||||||
Send
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react';
|
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
||||||
import { Fragment, useState } from 'react';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
|
|
||||||
import { CancelIcon, HideIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useChannelMessages } from '@stores/channels';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/date';
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function MessageHideButton({ id }: { id: string }) {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const hide = useChannelMessages((state: any) => state.hideMessage);
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const { account } = useAccount();
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideMessage = () => {
|
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
|
||||||
ndk.signer = signer;
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
// build event
|
|
||||||
event.content = '';
|
|
||||||
event.kind = 43;
|
|
||||||
event.created_at = dateToUnix();
|
|
||||||
event.pubkey = account.pubkey;
|
|
||||||
event.tags = [['e', id]];
|
|
||||||
|
|
||||||
// publish event
|
|
||||||
event.publish();
|
|
||||||
|
|
||||||
// update state
|
|
||||||
hide(id);
|
|
||||||
|
|
||||||
// close modal
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={openModal}
|
|
||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
|
|
||||||
>
|
|
||||||
<HideIcon width={16} height={16} className="text-zinc-200" />
|
|
||||||
</button>
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
|
||||||
</Transition.Child>
|
|
||||||
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col rounded-lg border border-zinc-800 bg-zinc-900">
|
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-6">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-xl font-semibold leading-none text-transparent"
|
|
||||||
>
|
|
||||||
Are you sure!
|
|
||||||
</Dialog.Title>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Dialog.Description className="leading-tight text-white/50">
|
|
||||||
This message will be hidden from your feed.
|
|
||||||
</Dialog.Description>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full w-full flex-col items-end justify-center overflow-y-auto px-5 py-2.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-base font-medium text-white/50 hover:bg-zinc-800 hover:text-white"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => hideMessage()}
|
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-white hover:bg-red-600"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import { MessageHideButton } from '@app/channel/components/messages/hideButton';
|
|
||||||
import { MessageMuteButton } from '@app/channel/components/messages/muteButton';
|
|
||||||
import { MessageReplyButton } from '@app/channel/components/messages/replyButton';
|
|
||||||
|
|
||||||
import { MentionNote } from '@shared/notes/mentions/note';
|
|
||||||
import { ImagePreview } from '@shared/notes/preview/image';
|
|
||||||
import { LinkPreview } from '@shared/notes/preview/link';
|
|
||||||
import { VideoPreview } from '@shared/notes/preview/video';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
import { parser } from '@utils/parser';
|
|
||||||
import { LumeEvent } from '@utils/types';
|
|
||||||
|
|
||||||
export function ChannelMessageItem({ data }: { data: LumeEvent }) {
|
|
||||||
const content = parser(data);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group relative flex h-min min-h-min w-full select-text flex-col px-5 py-3 hover:bg-black/20">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<User pubkey={data.pubkey} time={data.created_at} isChat={true} />
|
|
||||||
<div className="-mt-[20px] pl-[49px]">
|
|
||||||
<p className="select-text whitespace-pre-line break-words text-base text-white">
|
|
||||||
{content.parsed}
|
|
||||||
</p>
|
|
||||||
{Array.isArray(content.images) && content.images.length ? (
|
|
||||||
<ImagePreview urls={content.images} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{Array.isArray(content.videos) && content.videos.length ? (
|
|
||||||
<VideoPreview urls={content.videos} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{Array.isArray(content.links) && content.links.length ? (
|
|
||||||
<LinkPreview urls={content.links} />
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{Array.isArray(content.notes) && content.notes.length ? (
|
|
||||||
content.notes.map((note: string) => <MentionNote key={note} id={note} />)
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="absolute -top-4 right-4 z-10 hidden group-hover:inline-flex">
|
|
||||||
<div className="inline-flex h-8 items-center justify-center gap-1.5 rounded bg-zinc-900 px-0.5 shadow-md shadow-black/20 ring-1 ring-zinc-800">
|
|
||||||
<MessageReplyButton id={data.id} pubkey={data.pubkey} content={data.content} />
|
|
||||||
<MessageHideButton id={data.id} />
|
|
||||||
<MessageMuteButton pubkey={data.pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,132 +0,0 @@
|
|||||||
import { Dialog, Transition } from '@headlessui/react';
|
|
||||||
import { NDKEvent, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
|
|
||||||
import { Fragment, useContext, useState } from 'react';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
|
|
||||||
import { CancelIcon, MuteIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useChannelMessages } from '@stores/channels';
|
|
||||||
|
|
||||||
import { dateToUnix } from '@utils/date';
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function MessageMuteButton({ pubkey }: { pubkey: string }) {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const mute = useChannelMessages((state: any) => state.muteUser);
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const { account } = useAccount();
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const muteUser = () => {
|
|
||||||
const signer = new NDKPrivateKeySigner(account.privkey);
|
|
||||||
ndk.signer = signer;
|
|
||||||
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
// build event
|
|
||||||
event.content = '';
|
|
||||||
event.kind = 44;
|
|
||||||
event.created_at = dateToUnix();
|
|
||||||
event.pubkey = account.pubkey;
|
|
||||||
event.tags = [['p', pubkey]];
|
|
||||||
|
|
||||||
// publish event
|
|
||||||
event.publish();
|
|
||||||
|
|
||||||
// update state
|
|
||||||
mute(pubkey);
|
|
||||||
|
|
||||||
// close modal
|
|
||||||
closeModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openModal()}
|
|
||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
|
|
||||||
>
|
|
||||||
<MuteIcon width={16} height={16} className="text-zinc-200" />
|
|
||||||
</button>
|
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
|
||||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
|
||||||
</Transition.Child>
|
|
||||||
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col rounded-lg border border-zinc-800 bg-zinc-900">
|
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-6">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="bg-gradient-to-br from-zinc-200 to-zinc-400 bg-clip-text text-xl font-semibold leading-none text-transparent"
|
|
||||||
>
|
|
||||||
Are you sure!
|
|
||||||
</Dialog.Title>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<CancelIcon width={20} height={20} className="text-zinc-300" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Dialog.Description className="leading-tight text-white/50">
|
|
||||||
You will no longer see messages from this user.
|
|
||||||
</Dialog.Description>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-full w-full flex-col items-end justify-center overflow-y-auto px-5 py-2.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md px-2 text-base font-medium text-white/50 hover:bg-zinc-800 hover:text-white"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => muteUser()}
|
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md bg-red-500 px-2 text-base font-medium text-white hover:bg-red-600"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { ReplyMessageIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useChannelMessages } from '@stores/channels';
|
|
||||||
|
|
||||||
export function MessageReplyButton({
|
|
||||||
id,
|
|
||||||
pubkey,
|
|
||||||
content,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
pubkey: string;
|
|
||||||
content: string;
|
|
||||||
}) {
|
|
||||||
const openReply = useChannelMessages((state: any) => state.openReply);
|
|
||||||
|
|
||||||
const createReply = () => {
|
|
||||||
openReply(id, pubkey, content);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => createReply()}
|
|
||||||
className="inline-flex h-7 w-7 items-center justify-center rounded hover:bg-zinc-800"
|
|
||||||
>
|
|
||||||
<ReplyMessageIcon width={16} height={16} className="text-zinc-200" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
|
||||||
|
|
||||||
export function ChannelMessageUserMute({ pubkey }: { pubkey: string }) {
|
|
||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{isError || isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="relative h-11 w-11 shrink animate-pulse rounded-md bg-zinc-800" />
|
|
||||||
<div className="flex w-full flex-1 items-center justify-between">
|
|
||||||
<div className="flex items-baseline gap-2 text-base">
|
|
||||||
<div className="h-4 w-20 animate-pulse rounded bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="relative h-11 w-11 shrink-0 rounded-md">
|
|
||||||
<Image
|
|
||||||
src={user?.image}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
className="h-11 w-11 rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-1 items-center justify-between">
|
|
||||||
<span className="leading-none text-zinc-300">
|
|
||||||
You has been muted this user
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
|
||||||
import { shortenKey } from '@utils/shortenKey';
|
|
||||||
|
|
||||||
export function UserReply({ pubkey }: { pubkey: string }) {
|
|
||||||
const { user, isError, isLoading } = useProfile(pubkey);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group flex items-start gap-2">
|
|
||||||
{isError || isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="relative h-9 w-9 shrink animate-pulse overflow-hidden rounded bg-zinc-800" />
|
|
||||||
<span className="h-2 w-10 animate-pulse rounded bg-zinc-800 text-base font-medium leading-none text-white/50" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="relative h-9 w-9 shrink overflow-hidden rounded">
|
|
||||||
<Image
|
|
||||||
src={user?.image}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt={pubkey}
|
|
||||||
className="h-9 w-9 rounded object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="max-w-[10rem] truncate text-sm font-medium leading-none text-white/50">
|
|
||||||
Replying to {user?.name || shortenKey(pubkey)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
|
|
||||||
import { useChannelProfile } from '@app/channel/hooks/useChannelProfile';
|
|
||||||
|
|
||||||
import { CopyIcon } from '@shared/icons';
|
|
||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
|
|
||||||
export function ChannelMetadata({ id }: { id: string }) {
|
|
||||||
const metadata = useChannelProfile(id);
|
|
||||||
const noteID = id ? nip19.noteEncode(id) : null;
|
|
||||||
|
|
||||||
const copyNoteID = async () => {
|
|
||||||
const { writeText } = await import('@tauri-apps/plugin-clipboard-manager');
|
|
||||||
if (noteID) {
|
|
||||||
await writeText(noteID);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="relative h-11 w-11 shrink-0 rounded-md">
|
|
||||||
<Image
|
|
||||||
src={metadata?.picture}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt={id}
|
|
||||||
className="h-11 w-11 rounded-md bg-zinc-900 object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="inline-flex items-center gap-1">
|
|
||||||
<h5 className="text-lg font-semibold leading-none">{metadata?.name}</h5>
|
|
||||||
<button type="button" onClick={() => copyNoteID()}>
|
|
||||||
<CopyIcon width={14} height={14} className="text-white/50" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="leading-tight text-white/50">
|
|
||||||
{metadata?.about || (noteID && `${noteID.substring(0, 24)}...`)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
|
||||||
import { shortenKey } from '@utils/shortenKey';
|
|
||||||
|
|
||||||
export function MutedItem({ data }: { data: any }) {
|
|
||||||
const { user, isError, isLoading } = useProfile(data.content);
|
|
||||||
const [status, setStatus] = useState(data.status);
|
|
||||||
|
|
||||||
const unmute = async () => {
|
|
||||||
const { updateItemInBlacklist } = await import('@libs/storage');
|
|
||||||
const res = await updateItemInBlacklist(data.content, 0);
|
|
||||||
if (res) {
|
|
||||||
setStatus(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mute = async () => {
|
|
||||||
const { updateItemInBlacklist } = await import('@libs/storage');
|
|
||||||
const res = await updateItemInBlacklist(data.content, 1);
|
|
||||||
if (res) {
|
|
||||||
setStatus(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{isError || isLoading ? (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<div className="relative h-9 w-9 shrink animate-pulse rounded-md bg-zinc-800" />
|
|
||||||
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
|
||||||
<div className="h-3 w-16 animate-pulse bg-zinc-800" />
|
|
||||||
<div className="h-2 w-10 animate-pulse bg-zinc-800" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<div className="relative h-9 w-9 shrink rounded-md">
|
|
||||||
<Image
|
|
||||||
src={user?.image}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt={data.content}
|
|
||||||
className="h-9 w-9 rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-1 flex-col items-start gap-0.5 text-start">
|
|
||||||
<span className="truncate text-base font-medium leading-none text-white">
|
|
||||||
{user?.displayName || user?.name || 'Pleb'}
|
|
||||||
</span>
|
|
||||||
<span className="text-base leading-none text-white/50">
|
|
||||||
{shortenKey(data.content)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{status === 1 ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => unmute()}
|
|
||||||
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-white/50 hover:bg-zinc-800 hover:text-fuchsia-500"
|
|
||||||
>
|
|
||||||
Unmute
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => mute()}
|
|
||||||
className="inline-flex h-6 w-min items-center justify-center rounded px-1.5 text-base font-medium leading-none text-white/50 hover:bg-zinc-800 hover:text-fuchsia-500"
|
|
||||||
>
|
|
||||||
Mute
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { getChannel, updateChannelMetadata } from '@libs/storage';
|
|
||||||
|
|
||||||
export function useChannelProfile(id: string) {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { data } = useQuery(['channel-metadata', id], async () => {
|
|
||||||
return await getChannel(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// subscribe to channel
|
|
||||||
const sub = ndk.subscribe(
|
|
||||||
{
|
|
||||||
'#e': [id],
|
|
||||||
kinds: [41],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
closeOnEose: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
sub.addListener('event', (event: { content: string }) => {
|
|
||||||
// update in local database
|
|
||||||
updateChannelMetadata(id, event.content);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
import { useCallback, useContext, useEffect, useLayoutEffect, useRef } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
|
||||||
|
|
||||||
import { ChannelMembers } from '@app/channel/components/members';
|
|
||||||
import { ChannelMessageForm } from '@app/channel/components/messages/form';
|
|
||||||
import { ChannelMetadata } from '@app/channel/components/metadata';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
|
|
||||||
import { useChannelMessages } from '@stores/channels';
|
|
||||||
|
|
||||||
import { dateToUnix, getHourAgo } from '@utils/date';
|
|
||||||
import { LumeEvent } from '@utils/types';
|
|
||||||
|
|
||||||
import { ChannelMessageItem } from './components/messages/item';
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const Header = (
|
|
||||||
<div className="relative py-4">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<div className="w-full border-t border-zinc-800" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<div className="inline-flex items-center gap-x-1.5 rounded-full bg-zinc-900 px-3 py-1.5 text-sm font-medium text-white/50 shadow-sm ring-1 ring-inset ring-zinc-800">
|
|
||||||
{getHourAgo(24, now).toLocaleDateString('en-US', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Empty = (
|
|
||||||
<div className="flex flex-col gap-1 text-center">
|
|
||||||
<h3 className="text-base font-semibold leading-none text-white">
|
|
||||||
Nothing to see here yet
|
|
||||||
</h3>
|
|
||||||
<p className="text-base leading-none text-white/50">
|
|
||||||
Be the first to share a message in this channel.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export function ChannelScreen() {
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const virtuosoRef = useRef(null);
|
|
||||||
|
|
||||||
const { id } = useParams();
|
|
||||||
|
|
||||||
const [messages, fetchMessages, addMessage, clearMessages] = useChannelMessages(
|
|
||||||
(state: any) => [state.messages, state.fetch, state.add, state.clear]
|
|
||||||
);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
fetchMessages(id);
|
|
||||||
}, [fetchMessages]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// subscribe to channel
|
|
||||||
const sub = ndk.subscribe(
|
|
||||||
{
|
|
||||||
'#e': [id],
|
|
||||||
kinds: [42],
|
|
||||||
since: dateToUnix(),
|
|
||||||
},
|
|
||||||
{ closeOnEose: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
sub.addListener('event', (event: LumeEvent) => {
|
|
||||||
addMessage(id, event);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearMessages();
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const itemContent: any = useCallback(
|
|
||||||
(index: string | number) => {
|
|
||||||
return <ChannelMessageItem data={messages[index]} />;
|
|
||||||
},
|
|
||||||
[messages]
|
|
||||||
);
|
|
||||||
|
|
||||||
const computeItemKey = useCallback(
|
|
||||||
(index: string | number) => {
|
|
||||||
return messages[index].event_id;
|
|
||||||
},
|
|
||||||
[messages]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid h-full w-full grid-cols-3">
|
|
||||||
<div className="col-span-2 flex flex-col justify-between border-r border-zinc-900">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
|
|
||||||
>
|
|
||||||
<h3 className="font-semibold text-white">Public Channel</h3>
|
|
||||||
</div>
|
|
||||||
<div className="h-full w-full flex-1 p-3">
|
|
||||||
<div className="flex h-full flex-col justify-between overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900">
|
|
||||||
<div className="h-full w-full flex-1">
|
|
||||||
{!messages ? (
|
|
||||||
<p>Loading...</p>
|
|
||||||
) : (
|
|
||||||
<Virtuoso
|
|
||||||
ref={virtuosoRef}
|
|
||||||
data={messages}
|
|
||||||
itemContent={itemContent}
|
|
||||||
computeItemKey={computeItemKey}
|
|
||||||
initialTopMostItemIndex={messages.length - 1}
|
|
||||||
alignToBottom={true}
|
|
||||||
followOutput={true}
|
|
||||||
overscan={50}
|
|
||||||
increaseViewportBy={{ top: 200, bottom: 200 }}
|
|
||||||
className="scrollbar-hide overflow-y-auto"
|
|
||||||
components={{
|
|
||||||
Header: () => Header,
|
|
||||||
EmptyPlaceholder: () => Empty,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="z-50 shrink-0 rounded-b-xl border-t border-zinc-800 bg-zinc-900 p-3 px-5">
|
|
||||||
<ChannelMessageForm channelID={id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1 flex flex-col">
|
|
||||||
<div
|
|
||||||
data-tauri-drag-region
|
|
||||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center border-b border-zinc-900"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-3 p-3">
|
|
||||||
<ChannelMetadata id={id} />
|
|
||||||
<ChannelMembers id={id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -6,17 +6,14 @@ import { NewMessageModal } from '@app/chats/components/modal';
|
|||||||
import { ChatsListSelfItem } from '@app/chats/components/self';
|
import { ChatsListSelfItem } from '@app/chats/components/self';
|
||||||
import { UnknownsModal } from '@app/chats/components/unknowns';
|
import { UnknownsModal } from '@app/chats/components/unknowns';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { getChats } from '@libs/storage';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { Chats } from '@utils/types';
|
import { Chats } from '@utils/types';
|
||||||
|
|
||||||
export function ChatsList() {
|
export function ChatsList() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, data: chats } = useQuery(['chats'], async () => {
|
const { status, data: chats } = useQuery(['chats'], async () => {
|
||||||
return await getChats();
|
return { follows: [], unknowns: [] };
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { NDKSubscription } from '@nostr-dev-kit/ndk';
|
import { NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
@ -9,22 +9,18 @@ import { ChatMessageItem } from '@app/chats/components/messages/item';
|
|||||||
import { ChatSidebar } from '@app/chats/components/sidebar';
|
import { ChatSidebar } from '@app/chats/components/sidebar';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { createChat, getChatMessages } from '@libs/storage';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
import { Chats } from '@utils/types';
|
|
||||||
|
|
||||||
export function ChatScreen() {
|
export function ChatScreen() {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const virtuosoRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const { pubkey } = useParams();
|
const { pubkey } = useParams();
|
||||||
const { status, data } = useQuery(['chat', pubkey], async () => {
|
const { status, data } = useQuery(['chat', pubkey], async () => {
|
||||||
return await getChatMessages(db.account.pubkey, pubkey);
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const userPrivkey = useStronghold((state) => state.privkey);
|
const userPrivkey = useStronghold((state) => state.privkey);
|
||||||
@ -49,22 +45,6 @@ export function ChatScreen() {
|
|||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const chat = useMutation({
|
|
||||||
mutationFn: (data: Chats) => {
|
|
||||||
return createChat(
|
|
||||||
data.id,
|
|
||||||
data.receiver_pubkey,
|
|
||||||
data.sender_pubkey,
|
|
||||||
data.content,
|
|
||||||
data.tags,
|
|
||||||
data.created_at
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['chat', pubkey] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub: NDKSubscription = ndk.subscribe(
|
const sub: NDKSubscription = ndk.subscribe(
|
||||||
{
|
{
|
||||||
@ -79,14 +59,7 @@ export function ChatScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
sub.addListener('event', (event) => {
|
sub.addListener('event', (event) => {
|
||||||
chat.mutate({
|
console.log(event);
|
||||||
id: event.id,
|
|
||||||
receiver_pubkey: pubkey,
|
|
||||||
sender_pubkey: event.pubkey,
|
|
||||||
content: event.content,
|
|
||||||
tags: event.tags,
|
|
||||||
created_at: event.created_at,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { EyeOffIcon, EyeOnIcon } from '@shared/icons';
|
import { EyeOffIcon, EyeOnIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function AccountSettingsScreen() {
|
export function AccountSettingsScreen() {
|
||||||
const { status, account } = useAccount();
|
|
||||||
const [type, setType] = useState('password');
|
const [type, setType] = useState('password');
|
||||||
|
|
||||||
const privkey = useStronghold((state) => state.privkey);
|
const privkey = useStronghold((state) => state.privkey);
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
const showPrivateKey = () => {
|
const showPrivateKey = () => {
|
||||||
if (type === 'password') {
|
if (type === 'password') {
|
||||||
@ -35,7 +34,7 @@ export function AccountSettingsScreen() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={account.pubkey}
|
value={db.account.pubkey}
|
||||||
className="relative w-2/3 rounded-lg bg-white/10 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
className="relative w-2/3 rounded-lg bg-white/10 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -45,7 +44,7 @@ export function AccountSettingsScreen() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
readOnly
|
readOnly
|
||||||
value={account.npub}
|
value={db.account.npub}
|
||||||
className="relative w-2/3 rounded-lg bg-white/10 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
className="relative w-2/3 rounded-lg bg-white/10 py-3 pl-3.5 pr-11 text-white !outline-none placeholder:text-white/50"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,26 +3,24 @@ import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
import { getSetting, updateSetting } from '@libs/storage';
|
|
||||||
|
|
||||||
export function AutoStartSetting() {
|
export function AutoStartSetting() {
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
const toggle = async () => {
|
const toggle = async () => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
await enable();
|
await enable();
|
||||||
await updateSetting('auto_start', 1);
|
// await updateSetting('auto_start', 1);
|
||||||
console.log(`registered for autostart? ${await isEnabled()}`);
|
console.log(`registered for autostart? ${await isEnabled()}`);
|
||||||
} else {
|
} else {
|
||||||
await disable();
|
await disable();
|
||||||
await updateSetting('auto_start', 0);
|
// await updateSetting('auto_start', 0);
|
||||||
}
|
}
|
||||||
setEnabled(!enabled);
|
setEnabled(!enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getAppSetting() {
|
async function getAppSetting() {
|
||||||
const setting = await getSetting('auto_start');
|
const setting = '0';
|
||||||
if (parseInt(setting) === 0) {
|
if (parseInt(setting) === 0) {
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { getSetting, updateSetting } from '@libs/storage';
|
|
||||||
|
|
||||||
import { CheckCircleIcon } from '@shared/icons';
|
import { CheckCircleIcon } from '@shared/icons';
|
||||||
|
|
||||||
const setting = await getSetting('cache_time');
|
|
||||||
const cacheTime = setting;
|
|
||||||
|
|
||||||
export function CacheTimeSetting() {
|
export function CacheTimeSetting() {
|
||||||
const [time, setTime] = useState(cacheTime);
|
const [time, setTime] = useState('0');
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
await updateSetting('cache_time', time);
|
// await updateSetting('cache_time', time);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,7 +10,7 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
|
|
||||||
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR, widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
export function FeedModal() {
|
export function FeedModal() {
|
||||||
@ -40,7 +40,7 @@ export function FeedModal() {
|
|||||||
|
|
||||||
// update state
|
// update state
|
||||||
setWidget(db, {
|
setWidget(db, {
|
||||||
kind: BLOCK_KINDS.feed,
|
kind: widgetKinds.feed,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
content: JSON.stringify(selected),
|
content: JSON.stringify(selected),
|
||||||
});
|
});
|
||||||
|
@ -6,7 +6,7 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
|
|
||||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
export function HashtagModal() {
|
export function HashtagModal() {
|
||||||
@ -28,7 +28,7 @@ export function HashtagModal() {
|
|||||||
|
|
||||||
// update state
|
// update state
|
||||||
setWidget(db, {
|
setWidget(db, {
|
||||||
kind: BLOCK_KINDS.hashtag,
|
kind: widgetKinds.hashtag,
|
||||||
title: data.hashtag,
|
title: data.hashtag,
|
||||||
content: data.hashtag.replace('#', ''),
|
content: data.hashtag.replace('#', ''),
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR, widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
import { useImageUploader } from '@utils/hooks/useUploader';
|
||||||
@ -40,7 +40,7 @@ export function ImageModal() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// mutate
|
// mutate
|
||||||
setWidget(db, { kind: BLOCK_KINDS.image, title: data.title, content: data.content });
|
setWidget(db, { kind: widgetKinds.image, title: data.title, content: data.content });
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
// reset form
|
// reset form
|
||||||
|
@ -2,8 +2,6 @@ import { useInfiniteQuery } from '@tanstack/react-query';
|
|||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { getNotesByAuthors } from '@libs/storage';
|
|
||||||
|
|
||||||
import { NoteKind_1, NoteKind_1063, NoteThread, Repost } from '@shared/notes';
|
import { NoteKind_1, NoteKind_1063, NoteThread, Repost } from '@shared/notes';
|
||||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
@ -11,14 +9,12 @@ import { TitleBar } from '@shared/titleBar';
|
|||||||
|
|
||||||
import { LumeEvent, Widget } from '@utils/types';
|
import { LumeEvent, Widget } from '@utils/types';
|
||||||
|
|
||||||
const ITEM_PER_PAGE = 10;
|
|
||||||
|
|
||||||
export function FeedBlock({ params }: { params: Widget }) {
|
export function FeedBlock({ params }: { params: Widget }) {
|
||||||
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||||
useInfiniteQuery({
|
useInfiniteQuery({
|
||||||
queryKey: ['newsfeed', params.content],
|
queryKey: ['newsfeed', params.content],
|
||||||
queryFn: async ({ pageParam = 0 }) => {
|
queryFn: async () => {
|
||||||
return await getNotesByAuthors(params.content, ITEM_PER_PAGE, pageParam);
|
return { data: [], nextCursor: 0 };
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
});
|
});
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import { CancelIcon } from '@shared/icons';
|
|
||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
|
||||||
import { useWidgets } from '@stores/widgets';
|
|
||||||
|
|
||||||
import { Widget } from '@utils/types';
|
|
||||||
|
|
||||||
export function ImageBlock({ params }: { params: Widget }) {
|
|
||||||
const remove = useWidgets((state) => state.removeWidget);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-[400px] shrink-0 flex-col justify-between">
|
|
||||||
<div className="relative h-full w-full flex-1 overflow-hidden p-3">
|
|
||||||
<div className="absolute left-0 top-3 h-16 w-full px-3">
|
|
||||||
<div className="flex h-16 items-center justify-between overflow-hidden rounded-t-xl px-5">
|
|
||||||
<h3 className="font-medium text-white drop-shadow-lg">{params.title}</h3>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => remove(params.id)}
|
|
||||||
className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-white/30 backdrop-blur-lg"
|
|
||||||
>
|
|
||||||
<CancelIcon width={16} height={16} className="text-white" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Image
|
|
||||||
src={params.content}
|
|
||||||
fallback={DEFAULT_AVATAR}
|
|
||||||
alt={params.title}
|
|
||||||
className="h-full w-full rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,9 +1,11 @@
|
|||||||
|
import { NDKFilter } from '@nostr-dev-kit/ndk';
|
||||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { NostrEvent } from 'nostr-fetch';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { NoteKind_1, NoteKind_1063, NoteThread, Repost } from '@shared/notes';
|
import { NoteKind_1, NoteKind_1063, NoteThread, Repost } from '@shared/notes';
|
||||||
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport';
|
||||||
import { NoteSkeleton } from '@shared/notes/skeleton';
|
import { NoteSkeleton } from '@shared/notes/skeleton';
|
||||||
@ -13,11 +15,12 @@ import { useNostr } from '@utils/hooks/useNostr';
|
|||||||
import { LumeEvent } from '@utils/types';
|
import { LumeEvent } from '@utils/types';
|
||||||
|
|
||||||
export function NetworkBlock() {
|
export function NetworkBlock() {
|
||||||
const { fetchNotes } = useNostr();
|
const { db } = useStorage();
|
||||||
|
const { sub, fetchNotes } = useNostr();
|
||||||
const { status, data, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
const { status, data, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
|
||||||
queryKey: ['network-widget'],
|
queryKey: ['network-widget'],
|
||||||
queryFn: async ({ pageParam = 24 }) => {
|
queryFn: async ({ pageParam = 24 }) => {
|
||||||
return await fetchNotes(pageParam);
|
return { data: [], nextCursor: 0 };
|
||||||
},
|
},
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
@ -26,8 +29,7 @@ export function NetworkBlock() {
|
|||||||
|
|
||||||
const parentRef = useRef();
|
const parentRef = useRef();
|
||||||
const notes = useMemo(
|
const notes = useMemo(
|
||||||
// @ts-expect-error, todo
|
() => (data ? data.pages.flatMap((d: { data: LumeEvent[] }) => d.data) : []),
|
||||||
() => (data ? data.pages.flatMap((d: { data: NostrEvent[] }) => d.data) : []),
|
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -37,10 +39,20 @@ export function NetworkBlock() {
|
|||||||
estimateSize: () => 500,
|
estimateSize: () => 500,
|
||||||
overscan: 2,
|
overscan: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
const itemsVirtualizer = rowVirtualizer.getVirtualItems();
|
||||||
const totalSize = rowVirtualizer.getTotalSize();
|
const totalSize = rowVirtualizer.getTotalSize();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const since = Math.floor(Date.now() / 1000);
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [1, 6],
|
||||||
|
authors: db.account.network,
|
||||||
|
since: since,
|
||||||
|
};
|
||||||
|
|
||||||
|
sub(filter, (event) => console.log('[network] event received: ', event));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(index: string | number) => {
|
(index: string | number) => {
|
||||||
const note: LumeEvent = notes[index];
|
const note: LumeEvent = notes[index];
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { createReplyNote } from '@libs/storage';
|
|
||||||
|
|
||||||
export function useLiveThread(id: string) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const now = useRef(Math.floor(Date.now() / 1000));
|
|
||||||
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
|
|
||||||
const thread = useMutation({
|
|
||||||
mutationFn: (data: NDKEvent) => {
|
|
||||||
return createReplyNote(
|
|
||||||
id,
|
|
||||||
data.id,
|
|
||||||
data.pubkey,
|
|
||||||
data.kind,
|
|
||||||
data.tags,
|
|
||||||
data.content,
|
|
||||||
data.created_at
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['replies', id] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const filter: NDKFilter = {
|
|
||||||
kinds: [1],
|
|
||||||
'#e': [id],
|
|
||||||
since: now.current,
|
|
||||||
};
|
|
||||||
|
|
||||||
const sub = ndk.subscribe(filter, { closeOnEose: false });
|
|
||||||
|
|
||||||
sub.addListener('event', (event: NDKEvent) => {
|
|
||||||
thread.mutate(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { createNote } from '@libs/storage';
|
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function useNewsfeed() {
|
|
||||||
const sub = useRef(null);
|
|
||||||
const now = useRef(Math.floor(Date.now() / 1000));
|
|
||||||
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, account } = useAccount();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === 'success' && account) {
|
|
||||||
const filter: NDKFilter = {
|
|
||||||
kinds: [1, 6],
|
|
||||||
authors: account.follows,
|
|
||||||
since: now.current,
|
|
||||||
};
|
|
||||||
|
|
||||||
sub.current = ndk.subscribe(filter, { closeOnEose: false });
|
|
||||||
|
|
||||||
sub.current.addListener('event', (event: NDKEvent) => {
|
|
||||||
// add to db
|
|
||||||
createNote(
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (sub.current) {
|
|
||||||
sub.current.stop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [status]);
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ import { HashtagModal } from '@app/space/components/modals/hashtag';
|
|||||||
import { ImageModal } from '@app/space/components/modals/image';
|
import { ImageModal } from '@app/space/components/modals/image';
|
||||||
import { FeedBlock } from '@app/space/components/widgets/feed';
|
import { FeedBlock } from '@app/space/components/widgets/feed';
|
||||||
import { HashtagBlock } from '@app/space/components/widgets/hashtag';
|
import { HashtagBlock } from '@app/space/components/widgets/hashtag';
|
||||||
import { ImageBlock } from '@app/space/components/widgets/image';
|
|
||||||
import { NetworkBlock } from '@app/space/components/widgets/network';
|
import { NetworkBlock } from '@app/space/components/widgets/network';
|
||||||
import { ThreadBlock } from '@app/space/components/widgets/thread';
|
import { ThreadBlock } from '@app/space/components/widgets/thread';
|
||||||
import { UserBlock } from '@app/space/components/widgets/user';
|
import { UserBlock } from '@app/space/components/widgets/user';
|
||||||
@ -19,18 +18,16 @@ import { useWidgets } from '@stores/widgets';
|
|||||||
import { Widget } from '@utils/types';
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
export function SpaceScreen() {
|
export function SpaceScreen() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
const [widgets, fetchWidgets] = useWidgets((state) => [
|
const [widgets, fetchWidgets] = useWidgets((state) => [
|
||||||
state.widgets,
|
state.widgets,
|
||||||
state.fetchWidgets,
|
state.fetchWidgets,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
(widget: Widget) => {
|
(widget: Widget) => {
|
||||||
switch (widget.kind) {
|
switch (widget.kind) {
|
||||||
case 0:
|
|
||||||
return <ImageBlock key={widget.id} params={widget} />;
|
|
||||||
case 1:
|
case 1:
|
||||||
return <FeedBlock key={widget.id} params={widget} />;
|
return <FeedBlock key={widget.id} params={widget} />;
|
||||||
case 2:
|
case 2:
|
||||||
@ -39,6 +36,8 @@ export function SpaceScreen() {
|
|||||||
return <HashtagBlock key={widget.id} params={widget} />;
|
return <HashtagBlock key={widget.id} params={widget} />;
|
||||||
case 5:
|
case 5:
|
||||||
return <UserBlock key={widget.id} params={widget} />;
|
return <UserBlock key={widget.id} params={widget} />;
|
||||||
|
case 9999:
|
||||||
|
return <NetworkBlock key={widget.id} />;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -52,7 +51,6 @@ export function SpaceScreen() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-hide flex h-full w-full flex-nowrap divide-x divide-white/5 overflow-x-auto overflow-y-hidden">
|
<div className="scrollbar-hide flex h-full w-full flex-nowrap divide-x divide-white/5 overflow-x-auto overflow-y-hidden">
|
||||||
<NetworkBlock />
|
|
||||||
{!widgets ? (
|
{!widgets ? (
|
||||||
<div className="flex w-[350px] shrink-0 flex-col">
|
<div className="flex w-[350px] shrink-0 flex-col">
|
||||||
<div className="flex w-full flex-1 items-center justify-center p-3">
|
<div className="flex w-full flex-1 items-center justify-center p-3">
|
||||||
|
@ -11,14 +11,15 @@ interface Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TrendingNotes() {
|
export function TrendingNotes() {
|
||||||
const { status, data, error } = useQuery(
|
const { status, data } = useQuery(
|
||||||
['trending-notes'],
|
['trending-notes'],
|
||||||
async () => {
|
async () => {
|
||||||
const res = await fetch('https://api.nostr.band/v0/trending/notes');
|
const res = await fetch('https://api.nostr.band/v0/trending/notes');
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Error');
|
throw new Error('failed to fecht trending notes');
|
||||||
}
|
}
|
||||||
const json: Response = await res.json();
|
const json: Response = await res.json();
|
||||||
|
if (!json.notes) return null;
|
||||||
return json.notes;
|
return json.notes;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -29,19 +30,18 @@ export function TrendingNotes() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('notes: ', data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
||||||
<TitleBar title="Trending Posts" />
|
<TitleBar title="Trending Posts" />
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{error && <p>Failed to fetch</p>}
|
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl bg-white/10 px-3 py-3">
|
<div className="rounded-xl bg-white/10 px-3 py-3">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : status === 'error' ? (
|
||||||
|
<p>Failed to fetch</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex w-full flex-col">
|
<div className="relative flex w-full flex-col">
|
||||||
{data.map((item) => (
|
{data.map((item) => (
|
||||||
|
@ -10,7 +10,7 @@ interface Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function TrendingProfiles() {
|
export function TrendingProfiles() {
|
||||||
const { status, data, error } = useQuery(
|
const { status, data } = useQuery(
|
||||||
['trending-profiles'],
|
['trending-profiles'],
|
||||||
async () => {
|
async () => {
|
||||||
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
const res = await fetch('https://api.nostr.band/v0/trending/profiles');
|
||||||
@ -18,6 +18,7 @@ export function TrendingProfiles() {
|
|||||||
throw new Error('Error');
|
throw new Error('Error');
|
||||||
}
|
}
|
||||||
const json: Response = await res.json();
|
const json: Response = await res.json();
|
||||||
|
if (!json.profiles) return null;
|
||||||
return json.profiles;
|
return json.profiles;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,22 +29,21 @@ export function TrendingProfiles() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('profiles: ', data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
<div className="scrollbar-hide relative h-full w-[400px] shrink-0 overflow-y-auto bg-white/10 pb-20">
|
||||||
<TitleBar title="Trending Profiles" />
|
<TitleBar title="Trending Profiles" />
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
{error && <p>Failed to fetch</p>}
|
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl bg-white/10 px-3 py-3">
|
<div className="rounded-xl bg-white/10 px-3 py-3">
|
||||||
<NoteSkeleton />
|
<NoteSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : status === 'error' ? (
|
||||||
|
<p>Failed to fetch</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex w-full flex-col gap-3 px-3 pt-1.5">
|
<div className="relative flex w-full flex-col gap-3 px-3 pt-1.5">
|
||||||
{data?.map((item) => (
|
{data.map((item) => (
|
||||||
<Profile key={item.pubkey} data={item} />
|
<Profile key={item.pubkey} data={item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,15 +21,15 @@ export default class TauriAdapter implements NDKCacheAdapter {
|
|||||||
for (const author of filter.authors) {
|
for (const author of filter.authors) {
|
||||||
for (const kind of filter.kinds) {
|
for (const kind of filter.kinds) {
|
||||||
const key = `${author}:${kind}`;
|
const key = `${author}:${kind}`;
|
||||||
promises.push(this.store.getEventByKey(key));
|
promises.concat(this.store.getALlEventByKey(key));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(promises);
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
if (result) {
|
if (result && result.event) {
|
||||||
console.log('cache hit: ', result);
|
console.log('cache hit: ', result.event);
|
||||||
const ndkEvent = new NDKEvent(
|
const ndkEvent = new NDKEvent(
|
||||||
subscription.ndk,
|
subscription.ndk,
|
||||||
JSON.parse(result.event as string)
|
JSON.parse(result.event as string)
|
||||||
|
@ -56,6 +56,7 @@ export const NDKInstance = () => {
|
|||||||
async function initNDK() {
|
async function initNDK() {
|
||||||
let explicitRelayUrls: string[];
|
let explicitRelayUrls: string[];
|
||||||
const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls();
|
const explicitRelayUrlsFromDB = await db.getExplicitRelayUrls();
|
||||||
|
console.log('relays in db: ', explicitRelayUrlsFromDB);
|
||||||
|
|
||||||
if (explicitRelayUrlsFromDB) {
|
if (explicitRelayUrlsFromDB) {
|
||||||
explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
|
explicitRelayUrls = await verifyRelays(explicitRelayUrlsFromDB);
|
||||||
|
@ -1,435 +0,0 @@
|
|||||||
import Database from '@tauri-apps/plugin-sql';
|
|
||||||
import { destr } from 'destr';
|
|
||||||
|
|
||||||
import { parser } from '@utils/parser';
|
|
||||||
import { getParentID } from '@utils/transform';
|
|
||||||
import {
|
|
||||||
Account,
|
|
||||||
Chats,
|
|
||||||
LumeEvent,
|
|
||||||
Profile,
|
|
||||||
Relays,
|
|
||||||
Settings,
|
|
||||||
Widget,
|
|
||||||
} from '@utils/types';
|
|
||||||
|
|
||||||
let db: null | Database = null;
|
|
||||||
|
|
||||||
// connect database (sqlite)
|
|
||||||
// path: tauri::api::path::BaseDirectory::App
|
|
||||||
export async function connect(): Promise<Database> {
|
|
||||||
if (db) {
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
db = await Database.load('sqlite:lume.db');
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('Failed to connect to database, error: ', e);
|
|
||||||
}
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get active account
|
|
||||||
export async function getActiveAccount() {
|
|
||||||
const db = await connect();
|
|
||||||
const result: Array<Account> = await db.select(
|
|
||||||
'SELECT * FROM accounts WHERE is_active = 1;'
|
|
||||||
);
|
|
||||||
if (result.length > 0) {
|
|
||||||
return result[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create account
|
|
||||||
export async function createAccount(npub: string, pubkey: string, follows?: string[][]) {
|
|
||||||
const db = await connect();
|
|
||||||
const res = await db.execute(
|
|
||||||
'INSERT OR IGNORE INTO accounts (npub, pubkey, privkey, follows, is_active) VALUES (?, ?, ?, ?, ?);',
|
|
||||||
[npub, pubkey, 'privkey is stored in secure storage', follows || '', 1]
|
|
||||||
);
|
|
||||||
if (res) {
|
|
||||||
await createWidget(
|
|
||||||
0,
|
|
||||||
'Have fun together!',
|
|
||||||
'https://void.cat/d/N5KUHEQCVg7SywXUPiJ7yq.jpg'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const getAccount = await getActiveAccount();
|
|
||||||
return getAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update account
|
|
||||||
export async function updateAccount(column: string, value: string | string[]) {
|
|
||||||
const db = await connect();
|
|
||||||
const account = await getActiveAccount();
|
|
||||||
return await db.execute(`UPDATE accounts SET ${column} = ? WHERE id = ?;`, [
|
|
||||||
value,
|
|
||||||
account.id,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// count total notes
|
|
||||||
export async function countTotalNotes() {
|
|
||||||
const db = await connect();
|
|
||||||
const result: Array<{ total: string }> = await db.select(
|
|
||||||
'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);'
|
|
||||||
);
|
|
||||||
return parseInt(result[0].total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all notes
|
|
||||||
export async function getNotes(limit: number, offset: number) {
|
|
||||||
const db = await connect();
|
|
||||||
const totalNotes = await countTotalNotes();
|
|
||||||
const nextCursor = offset + limit;
|
|
||||||
|
|
||||||
const notes: { data: LumeEvent[] | null; nextCursor: number } = {
|
|
||||||
data: null,
|
|
||||||
nextCursor: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const query: LumeEvent[] = await db.select(
|
|
||||||
`SELECT * FROM notes WHERE kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`
|
|
||||||
);
|
|
||||||
|
|
||||||
query.forEach(
|
|
||||||
(el) => (el.tags = typeof el.tags === 'string' ? destr(el.tags) : el.tags)
|
|
||||||
);
|
|
||||||
|
|
||||||
notes['data'] = query;
|
|
||||||
notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
|
|
||||||
|
|
||||||
return notes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all notes by authors
|
|
||||||
export async function getNotesByAuthors(authors: string, limit: number, offset: number) {
|
|
||||||
const db = await connect();
|
|
||||||
const totalNotes = await countTotalNotes();
|
|
||||||
const nextCursor = offset + limit;
|
|
||||||
const array = JSON.parse(authors);
|
|
||||||
const finalArray = `'${array.join("','")}'`;
|
|
||||||
|
|
||||||
const notes: { data: LumeEvent[] | null; nextCursor: number } = {
|
|
||||||
data: null,
|
|
||||||
nextCursor: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const query: LumeEvent[] = await db.select(
|
|
||||||
`SELECT * FROM notes WHERE pubkey IN (${finalArray}) AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`
|
|
||||||
);
|
|
||||||
|
|
||||||
query.forEach(
|
|
||||||
(el) => (el.tags = typeof el.tags === 'string' ? destr(el.tags) : el.tags)
|
|
||||||
);
|
|
||||||
|
|
||||||
notes['data'] = query;
|
|
||||||
notes['nextCursor'] = Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
|
|
||||||
|
|
||||||
return notes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get note by id
|
|
||||||
export async function getNoteByID(event_id: string) {
|
|
||||||
const db = await connect();
|
|
||||||
const result: LumeEvent[] = await db.select(
|
|
||||||
`SELECT * FROM notes WHERE event_id = "${event_id}";`
|
|
||||||
);
|
|
||||||
if (result[0]) {
|
|
||||||
if (result[0].kind === 1) result[0]['content'] = parser(result[0]);
|
|
||||||
return result[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create note
|
|
||||||
export async function createNote(
|
|
||||||
event_id: string,
|
|
||||||
pubkey: string,
|
|
||||||
kind: number,
|
|
||||||
tags: string[][],
|
|
||||||
content: string,
|
|
||||||
created_at: number
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
|
||||||
const account = await getActiveAccount();
|
|
||||||
const parentID = getParentID(tags, event_id);
|
|
||||||
|
|
||||||
return await db.execute(
|
|
||||||
'INSERT OR IGNORE INTO notes (event_id, account_id, pubkey, kind, tags, content, created_at, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?);',
|
|
||||||
[event_id, account.id, pubkey, kind, tags, content, created_at, parentID]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get note replies
|
|
||||||
export async function getReplies(parent_id: string) {
|
|
||||||
const db = await connect();
|
|
||||||
const result: Array<LumeEvent> = await db.select(
|
|
||||||
`SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create reply note
|
|
||||||
export async function createReplyNote(
|
|
||||||
parent_id: string,
|
|
||||||
event_id: string,
|
|
||||||
pubkey: string,
|
|
||||||
kind: number,
|
|
||||||
tags: string[][],
|
|
||||||
content: string,
|
|
||||||
created_at: number
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
|
||||||
return await db.execute(
|
|
||||||
'INSERT OR IGNORE INTO replies (event_id, parent_id, pubkey, kind, tags, content, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);',
|
|
||||||
[event_id, parent_id, pubkey, kind, tags, content, created_at]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all chats by pubkey
|
|
||||||
export async function getChats() {
|
|
||||||
const db = await connect();
|
|
||||||
const account = await getActiveAccount();
|
|
||||||
|
|
||||||
const chats: { follows: Array<Chats> | null; unknowns: Array<Chats> | null } = {
|
|
||||||
follows: [],
|
|
||||||
unknowns: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let result: Array<Chats> = await db.select(
|
|
||||||
`SELECT DISTINCT sender_pubkey FROM chats WHERE receiver_pubkey = "${account.pubkey}" ORDER BY created_at DESC;`
|
|
||||||
);
|
|
||||||
|
|
||||||
result = result.map((v) => ({ ...v, new_messages: 0 }));
|
|
||||||
result = result.sort((a, b) => a.new_messages - b.new_messages);
|
|
||||||
|
|
||||||
chats.follows = result.filter((el) => {
|
|
||||||
return account.follows.some((i) => {
|
|
||||||
return i === el.sender_pubkey;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
chats.unknowns = result.filter(
|
|
||||||
(el) => !chats.follows.includes(el) && el.sender_pubkey !== account.pubkey
|
|
||||||
);
|
|
||||||
|
|
||||||
return chats;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get chat messages
|
|
||||||
export async function getChatMessages(receiver_pubkey: string, sender_pubkey: string) {
|
|
||||||
const db = await connect();
|
|
||||||
let receiver = [];
|
|
||||||
|
|
||||||
const sender: Array<Chats> = await db.select(
|
|
||||||
`SELECT * FROM chats WHERE sender_pubkey = "${sender_pubkey}" AND receiver_pubkey = "${receiver_pubkey}";`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (receiver_pubkey !== sender_pubkey) {
|
|
||||||
receiver = await db.select(
|
|
||||||
`SELECT * FROM chats WHERE sender_pubkey = "${receiver_pubkey}" AND receiver_pubkey = "${sender_pubkey}";`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = [...sender, ...receiver].sort(
|
|
||||||
(x: { created_at: number }, y: { created_at: number }) => x.created_at - y.created_at
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create chat
|
|
||||||
export async function createChat(
|
|
||||||
event_id: string,
|
|
||||||
receiver_pubkey: string,
|
|
||||||
sender_pubkey: string,
|
|
||||||
content: string,
|
|
||||||
tags: string[][],
|
|
||||||
created_at: number
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
|
||||||
await db.execute(
|
|
||||||
'INSERT OR IGNORE INTO chats (event_id, receiver_pubkey, sender_pubkey, content, tags, created_at) VALUES (?, ?, ?, ?, ?, ?);',
|
|
||||||
[event_id, receiver_pubkey, sender_pubkey, content, tags, created_at]
|
|
||||||
);
|
|
||||||
return sender_pubkey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get setting
|
|
||||||
export async function getSetting(key: string) {
|
|
||||||
const db = await connect();
|
|
||||||
const result: Array<Settings> = await db.select(
|
|
||||||
`SELECT value FROM settings WHERE key = "${key}";`
|
|
||||||
);
|
|
||||||
return result[0]?.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update setting
|
|
||||||
export async function updateSetting(key: string, value: string | number) {
|
|
||||||
const db = await connect();
|
|
||||||
return await db.execute(`UPDATE settings SET value = "${value}" WHERE key = "${key}";`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get last login
|
|
||||||
export async function getLastLogin() {
|
|
||||||
const db = await connect();
|
|
||||||
const result: Array<Settings> = await db.select(
|
|
||||||
`SELECT value FROM settings WHERE key = "last_login";`
|
|
||||||
);
|
|
||||||
if (result[0]) {
|
|
||||||
return parseInt(result[0].value);
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update last login
|
|
||||||
export async function updateLastLogin(value: number) {
|
|
||||||
const db = await connect();
|
|
||||||
return await db.execute(
|
|
||||||
`UPDATE settings SET value = ${value} WHERE key = "last_login";`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all widgets
|
|
||||||
export async function getWidgets() {
|
|
||||||
const db = await connect();
|
|
||||||
const account = await getActiveAccount();
|
|
||||||
const result: Array<Widget> = await db.select(
|
|
||||||
`SELECT * FROM widgets WHERE account_id = "${account.id}" ORDER BY created_at DESC;`
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create block
|
|
||||||
export async function createWidget(
|
|
||||||
kind: number,
|
|
||||||
title: string,
|
|
||||||
content: string | string[]
|
|
||||||
) {
|
|
||||||
const db = await connect();
|
|
||||||
const activeAccount = await getActiveAccount();
|
|
||||||
const insert = await db.execute(
|
|
||||||
'INSERT OR IGNORE INTO widgets (account_id, kind, title, content) VALUES (?, ?, ?, ?);',
|
|
||||||
[activeAccount.id, kind, title, content]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (insert) {
|
|
||||||
const record: Widget = await db.select(
|
|
||||||
'SELECT * FROM widgets ORDER BY id DESC LIMIT 1;'
|
|
||||||
);
|
|
||||||
return record[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove block
|
|
||||||
export async function removeWidget(id: string) {
|
|
||||||
const db = await connect();
|
|
||||||
return await db.execute(`DELETE FROM widgets WHERE id = "${id}";`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// logout
|
|
||||||
export async function removeAll() {
|
|
||||||
const db = await connect();
|
|
||||||
await db.execute(`UPDATE settings SET value = "0" WHERE key = "last_login";`);
|
|
||||||
await db.execute('DELETE FROM replies;');
|
|
||||||
await db.execute('DELETE FROM notes;');
|
|
||||||
await db.execute('DELETE FROM widgets;');
|
|
||||||
await db.execute('DELETE FROM chats;');
|
|
||||||
await db.execute('DELETE FROM accounts;');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create metadata
|
|
||||||
export async function createMetadata(id: string, pubkey: string, content: string) {
|
|
||||||
const db = await connect();
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
return await db.execute(
|
|
||||||
'INSERT OR REPLACE INTO metadata (id, pubkey, content, created_at) VALUES (?, ?, ?, ?);',
|
|
||||||
[id, pubkey, content, now]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllMetadata() {
|
|
||||||
const db = await connect();
|
|
||||||
const result: LumeEvent[] = await db.select(`SELECT * FROM metadata;`);
|
|
||||||
const users: Profile[] = result.map((el) => {
|
|
||||||
const profile: Profile = destr(el.content);
|
|
||||||
return {
|
|
||||||
pubkey: el.pubkey,
|
|
||||||
ident: profile.name || profile.display_name || profile.username || 'anon',
|
|
||||||
picture:
|
|
||||||
profile.picture ||
|
|
||||||
profile.image ||
|
|
||||||
'https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih.jpg',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get user metadata
|
|
||||||
export async function getUserMetadata(pubkey: string) {
|
|
||||||
const db = await connect();
|
|
||||||
const result = await db.select(`SELECT * FROM metadata WHERE pubkey = "${pubkey}";`);
|
|
||||||
if (result[0]) {
|
|
||||||
return { ...result[0], ...JSON.parse(result[0].content) } as Profile;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete privkey
|
|
||||||
export async function removePrivkey() {
|
|
||||||
const db = await connect();
|
|
||||||
const activeAccount = await getActiveAccount();
|
|
||||||
return await db.execute(
|
|
||||||
`UPDATE accounts SET privkey = "privkey is stored in secure storage" WHERE id = "${activeAccount.id}";`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get relays
|
|
||||||
export async function getRelays() {
|
|
||||||
const db = await connect();
|
|
||||||
const activeAccount = await getActiveAccount();
|
|
||||||
return (await db.select(
|
|
||||||
`SELECT * FROM relays WHERE account_id = "${activeAccount.id}";`
|
|
||||||
)) as Relays[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// get relays
|
|
||||||
export async function getExplicitRelayUrls() {
|
|
||||||
const db = await connect();
|
|
||||||
const activeAccount = await getActiveAccount();
|
|
||||||
|
|
||||||
if (!activeAccount) return null;
|
|
||||||
|
|
||||||
const result: Relays[] = await db.select(
|
|
||||||
`SELECT * FROM relays WHERE account_id = "${activeAccount.id}";`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.length > 0) return result.map((el) => el.relay);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create relay
|
|
||||||
export async function createRelay(relay: string, purpose?: string) {
|
|
||||||
const db = await connect();
|
|
||||||
const activeAccount = await getActiveAccount();
|
|
||||||
return await db.execute(
|
|
||||||
'INSERT OR IGNORE INTO relays (account_id, relay, purpose) VALUES (?, ?, ?);',
|
|
||||||
[activeAccount.id, relay, purpose || '']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove relay
|
|
||||||
export async function removeRelay(relay: string) {
|
|
||||||
const db = await connect();
|
|
||||||
return await db.execute(`DELETE FROM relays WHERE relay = "${relay}";`);
|
|
||||||
}
|
|
@ -2,7 +2,7 @@ import { BaseDirectory, removeFile } from '@tauri-apps/plugin-fs';
|
|||||||
import Database from '@tauri-apps/plugin-sql';
|
import Database from '@tauri-apps/plugin-sql';
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
||||||
|
|
||||||
import { Account, Relays, Widget } from '@utils/types';
|
import { Account, LumeEvent, Relays, Widget } from '@utils/types';
|
||||||
|
|
||||||
export class LumeStorage {
|
export class LumeStorage {
|
||||||
public db: Database;
|
public db: Database;
|
||||||
@ -133,16 +133,14 @@ export class LumeStorage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEventByKey(cacheKey: string) {
|
public async getALlEventByKey(cacheKey: string) {
|
||||||
const event = await this.db.select(
|
const events: LumeEvent[] = await this.db.select(
|
||||||
'SELECT * FROM events WHERE cache_key = $1 ORDER BY id DESC LIMIT 1;',
|
'SELECT * FROM events WHERE cache_key = $1 ORDER BY id DESC;',
|
||||||
[cacheKey]
|
[cacheKey]
|
||||||
)?.[0];
|
);
|
||||||
if (!event) {
|
|
||||||
// console.error('failed to get event by cache_key: ', cacheKey);
|
if (events.length < 1) return null;
|
||||||
return null;
|
return events;
|
||||||
}
|
|
||||||
return event;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEventByID(id: string) {
|
public async getEventByID(id: string) {
|
||||||
@ -150,20 +148,20 @@ export class LumeStorage {
|
|||||||
'SELECT * FROM events WHERE event_id = $1 ORDER BY id DESC LIMIT 1;',
|
'SELECT * FROM events WHERE event_id = $1 ORDER BY id DESC LIMIT 1;',
|
||||||
[id]
|
[id]
|
||||||
)?.[0];
|
)?.[0];
|
||||||
if (!event) {
|
|
||||||
// console.error('failed to get event by id: ', id);
|
if (!event) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getExplicitRelayUrls() {
|
public async getExplicitRelayUrls() {
|
||||||
|
if (!this.account) return null;
|
||||||
|
|
||||||
const result: Relays[] = await this.db.select(
|
const result: Relays[] = await this.db.select(
|
||||||
`SELECT * FROM relays WHERE account_id = "${this.account.id}";`
|
`SELECT * FROM relays WHERE account_id = "${this.account.id}" ORDER BY id DESC LIMIT 50;`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.length > 0) return result.map((el) => el.relay);
|
if (result.length < 1) return null;
|
||||||
return null;
|
return result.map((el) => el.relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createRelay(relay: string, purpose?: string) {
|
public async createRelay(relay: string, purpose?: string) {
|
||||||
|
@ -1,92 +1,15 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { produce } from 'immer';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { createChat, getLastLogin } from '@libs/storage';
|
|
||||||
|
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
|
import { NetworkStatusIndicator } from '@shared/networkStatusIndicator';
|
||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { sendNativeNotification } from '@utils/notification';
|
|
||||||
|
|
||||||
const lastLogin = await getLastLogin();
|
|
||||||
|
|
||||||
export function ActiveAccount({ data }: { data: { pubkey: string; npub: string } }) {
|
export function ActiveAccount({ data }: { data: { pubkey: string; npub: string } }) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, user } = useProfile(data.pubkey);
|
const { status, user } = useProfile(data.pubkey);
|
||||||
|
|
||||||
const chat = useMutation({
|
|
||||||
mutationFn: (data: any) => {
|
|
||||||
return createChat(
|
|
||||||
data.id,
|
|
||||||
data.receiver_pubkey,
|
|
||||||
data.sender_pubkey,
|
|
||||||
data.content,
|
|
||||||
data.tags,
|
|
||||||
data.created_at
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: (data: any) => {
|
|
||||||
const prev = queryClient.getQueryData(['chats']);
|
|
||||||
const next = produce(prev, (draft: any) => {
|
|
||||||
const target = draft.findIndex(
|
|
||||||
(m: { sender_pubkey: string }) => m.sender_pubkey === data
|
|
||||||
);
|
|
||||||
if (target !== -1) {
|
|
||||||
draft[target]['new_messages'] = draft[target]['new_messages'] + 1 || 1;
|
|
||||||
} else {
|
|
||||||
draft.push({ sender_pubkey: data, new_messages: 1 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
queryClient.setQueryData(['chats'], next);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const since = lastLogin > 0 ? lastLogin : Math.floor(Date.now() / 1000);
|
|
||||||
const sub = ndk.subscribe(
|
|
||||||
{
|
|
||||||
kinds: [4],
|
|
||||||
'#p': [data.pubkey],
|
|
||||||
since: since,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
closeOnEose: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
sub.addListener('event', (event) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case 4:
|
|
||||||
// update state
|
|
||||||
chat.mutate({
|
|
||||||
id: event.id,
|
|
||||||
receiver_pubkey: data.pubkey,
|
|
||||||
sender_pubkey: event.pubkey,
|
|
||||||
content: event.content,
|
|
||||||
tags: event.tags,
|
|
||||||
created_at: event.created_at,
|
|
||||||
});
|
|
||||||
// send native notifiation
|
|
||||||
sendNativeNotification("You've received new message");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sub.stop();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return <div className="h-9 w-9 animate-pulse rounded-md bg-white/50" />;
|
return <div className="h-9 w-9 animate-pulse rounded-md bg-white/50" />;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { ReactRenderer } from '@tiptap/react';
|
import { ReactRenderer } from '@tiptap/react';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
|
|
||||||
import { getAllMetadata } from '@libs/storage';
|
|
||||||
|
|
||||||
import { MentionList } from '@shared/composer';
|
import { MentionList } from '@shared/composer';
|
||||||
|
|
||||||
export const Suggestion = {
|
export const Suggestion = {
|
||||||
items: async ({ query }) => {
|
items: async ({ query }) => {
|
||||||
const users = await getAllMetadata();
|
const users = [];
|
||||||
return users
|
return users
|
||||||
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
|
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
@ -12,14 +11,11 @@ import {
|
|||||||
} from '@shared/icons';
|
} from '@shared/icons';
|
||||||
|
|
||||||
import { useComposer } from '@stores/composer';
|
import { useComposer } from '@stores/composer';
|
||||||
import { COMPOSE_SHORTCUT } from '@stores/shortcuts';
|
|
||||||
|
|
||||||
export function ComposerModal() {
|
export function ComposerModal() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
|
const [toggle, open] = useComposer((state) => [state.toggleModal, state.open]);
|
||||||
|
|
||||||
useHotkeys(COMPOSE_SHORTCUT, () => toggle(true));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={open} onOpenChange={toggle}>
|
<Dialog.Root open={open} onOpenChange={toggle}>
|
||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
|
@ -4,6 +4,8 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { Fragment, useEffect, useState } from 'react';
|
import { Fragment, useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { AvatarUploader } from '@shared/avatarUploader';
|
import { AvatarUploader } from '@shared/avatarUploader';
|
||||||
import { BannerUploader } from '@shared/bannerUploader';
|
import { BannerUploader } from '@shared/bannerUploader';
|
||||||
import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
|
import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
|
||||||
@ -11,7 +13,6 @@ import { Image } from '@shared/image';
|
|||||||
|
|
||||||
import { DEFAULT_AVATAR } from '@stores/constants';
|
import { DEFAULT_AVATAR } from '@stores/constants';
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
|
||||||
export function EditProfileModal() {
|
export function EditProfileModal() {
|
||||||
@ -23,8 +24,8 @@ export function EditProfileModal() {
|
|||||||
const [banner, setBanner] = useState('');
|
const [banner, setBanner] = useState('');
|
||||||
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
||||||
|
|
||||||
|
const { db } = useStorage();
|
||||||
const { publish } = useNostr();
|
const { publish } = useNostr();
|
||||||
const { account } = useAccount();
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -33,7 +34,7 @@ export function EditProfileModal() {
|
|||||||
formState: { isValid, errors },
|
formState: { isValid, errors },
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: async () => {
|
defaultValues: async () => {
|
||||||
const res: any = queryClient.getQueryData(['user', account.pubkey]);
|
const res: any = queryClient.getQueryData(['user', db.account.pubkey]);
|
||||||
if (res.image) {
|
if (res.image) {
|
||||||
setPicture(res.image);
|
setPicture(res.image);
|
||||||
}
|
}
|
||||||
@ -70,7 +71,7 @@ export function EditProfileModal() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return false;
|
if (!res.ok) return false;
|
||||||
if (res.data.names[username] === account.pubkey) {
|
if (res.data.names[username] === db.account.pubkey) {
|
||||||
setNIP05((prev) => ({ ...prev, verified: true }));
|
setNIP05((prev) => ({ ...prev, verified: true }));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
@ -119,7 +120,7 @@ export function EditProfileModal() {
|
|||||||
if (event.id) {
|
if (event.id) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// invalid cache
|
// invalid cache
|
||||||
queryClient.invalidateQueries(['user', account.pubkey]);
|
queryClient.invalidateQueries(['user', db.account.pubkey]);
|
||||||
// reset form
|
// reset form
|
||||||
reset();
|
reset();
|
||||||
// reset state
|
// reset state
|
||||||
|
@ -1,27 +1,15 @@
|
|||||||
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 { relaunch } from '@tauri-apps/plugin-process';
|
import { relaunch } from '@tauri-apps/plugin-process';
|
||||||
import { Fragment, useState } from 'react';
|
|
||||||
|
|
||||||
import { removeAll } from '@libs/storage';
|
|
||||||
|
|
||||||
import { CancelIcon, LogoutIcon } from '@shared/icons';
|
import { CancelIcon, LogoutIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function Logout() {
|
export function Logout() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
// reset database
|
// reset database
|
||||||
await removeAll();
|
// await removeAll();
|
||||||
// reset react query
|
// reset react query
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
// navigate
|
// navigate
|
||||||
@ -69,7 +57,6 @@ export function Logout() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={closeModal}
|
|
||||||
className="inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium text-white/50 hover:bg-white/10"
|
className="inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium text-white/50 hover:bg-white/10"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import * as Tooltip from '@radix-ui/react-tooltip';
|
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { ThreadIcon } from '@shared/icons';
|
import { ThreadIcon } from '@shared/icons';
|
||||||
import { MoreActions } from '@shared/notes/actions/more';
|
import { MoreActions } from '@shared/notes/actions/more';
|
||||||
import { NoteReaction } from '@shared/notes/actions/reaction';
|
import { NoteReaction } from '@shared/notes/actions/reaction';
|
||||||
@ -7,7 +9,7 @@ import { NoteReply } from '@shared/notes/actions/reply';
|
|||||||
import { NoteRepost } from '@shared/notes/actions/repost';
|
import { NoteRepost } from '@shared/notes/actions/repost';
|
||||||
import { NoteZap } from '@shared/notes/actions/zap';
|
import { NoteZap } from '@shared/notes/actions/zap';
|
||||||
|
|
||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
export function NoteActions({
|
export function NoteActions({
|
||||||
@ -21,6 +23,7 @@ export function NoteActions({
|
|||||||
noOpenThread?: boolean;
|
noOpenThread?: boolean;
|
||||||
root?: string;
|
root?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const { db } = useStorage();
|
||||||
const setWidget = useWidgets((state) => state.setWidget);
|
const setWidget = useWidgets((state) => state.setWidget);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -40,8 +43,8 @@ export function NoteActions({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setWidget({
|
setWidget(db, {
|
||||||
kind: BLOCK_KINDS.thread,
|
kind: widgetKinds.thread,
|
||||||
title: 'Thread',
|
title: 'Thread',
|
||||||
content: id,
|
content: id,
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
export function Hashtag({ tag }: { tag: string }) {
|
export function Hashtag({ tag }: { tag: string }) {
|
||||||
@ -9,7 +9,7 @@ export function Hashtag({ tag }: { tag: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setWidget({
|
setWidget({
|
||||||
kind: BLOCK_KINDS.hashtag,
|
kind: widgetKinds.hashtag,
|
||||||
title: tag,
|
title: tag,
|
||||||
content: tag.replace('#', ''),
|
content: tag.replace('#', ''),
|
||||||
})
|
})
|
||||||
|
@ -24,11 +24,11 @@ export function NoteKind_1({
|
|||||||
<div className="w-11 shrink-0" />
|
<div className="w-11 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
<NoteContent content={content} />
|
<NoteContent content={content} />
|
||||||
<NoteActions id={event.event_id || event.id} pubkey={event.pubkey} />
|
<NoteActions id={event.id || event.id} pubkey={event.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!skipMetadata ? (
|
{!skipMetadata ? (
|
||||||
<NoteMetadata id={event.event_id || event.id} />
|
<NoteMetadata id={event.id || event.id} />
|
||||||
) : (
|
) : (
|
||||||
<div className="pb-3" />
|
<div className="pb-3" />
|
||||||
)}
|
)}
|
||||||
|
@ -27,10 +27,10 @@ export function NoteKind_1063({ event }: { event: LumeEvent }) {
|
|||||||
className="h-auto w-full rounded-lg object-cover"
|
className="h-auto w-full rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<NoteActions id={event.event_id} pubkey={event.pubkey} />
|
<NoteActions id={event.id} pubkey={event.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoteMetadata id={event.event_id} />
|
<NoteMetadata id={event.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ export function SubNote({ id, root }: { id: string; root?: string }) {
|
|||||||
<div className="w-11 shrink-0" />
|
<div className="w-11 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
<NoteContent content={data.content} long={data.kind === 30023} />
|
<NoteContent content={data.content} long={data.kind === 30023} />
|
||||||
<NoteActions id={data.event_id} pubkey={data.pubkey} root={root} />
|
<NoteActions id={data.id} pubkey={data.pubkey} root={root} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,10 +28,10 @@ export function NoteThread({
|
|||||||
<div className="w-11 shrink-0" />
|
<div className="w-11 shrink-0" />
|
||||||
<div className="relative z-20 flex-1">
|
<div className="relative z-20 flex-1">
|
||||||
<NoteContent content={content} />
|
<NoteContent content={content} />
|
||||||
<NoteActions id={event.event_id} pubkey={event.pubkey} />
|
<NoteActions id={event.id} pubkey={event.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoteMetadata id={event.event_id} />
|
<NoteMetadata id={event.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,10 +25,10 @@ export function NoteKindUnsupport({ event }: { event: LumeEvent }) {
|
|||||||
<p>{event.content.toString()}</p>
|
<p>{event.content.toString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoteActions id={event.event_id} pubkey={event.pubkey} />
|
<NoteActions id={event.id} pubkey={event.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NoteMetadata id={event.event_id} />
|
<NoteMetadata id={event.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@ import remarkGfm from 'remark-gfm';
|
|||||||
import { MentionUser, NoteSkeleton } from '@shared/notes';
|
import { MentionUser, NoteSkeleton } from '@shared/notes';
|
||||||
import { User } from '@shared/user';
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
@ -17,7 +17,7 @@ export const MentionNote = memo(function MentionNote({ id }: { id: string }) {
|
|||||||
const openThread = (event, thread: string) => {
|
const openThread = (event, thread: string) => {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection.toString().length === 0) {
|
if (selection.toString().length === 0) {
|
||||||
setWidget({ kind: BLOCK_KINDS.thread, title: 'Thread', content: thread });
|
setWidget({ kind: widgetKinds.thread, title: 'Thread', content: thread });
|
||||||
} else {
|
} else {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
@ -13,7 +13,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setWidget({
|
setWidget({
|
||||||
kind: BLOCK_KINDS.user,
|
kind: widgetKinds.user,
|
||||||
title: user?.nip05 || user?.name || user?.display_name,
|
title: user?.nip05 || user?.name || user?.display_name,
|
||||||
content: pubkey,
|
content: pubkey,
|
||||||
})
|
})
|
||||||
|
@ -3,12 +3,12 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { decode } from 'light-bolt11-decoder';
|
import { decode } from 'light-bolt11-decoder';
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { createReplyNote } from '@libs/storage';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
import { LoaderIcon } from '@shared/icons';
|
||||||
import { MiniUser } from '@shared/notes/users/mini';
|
import { MiniUser } from '@shared/notes/users/mini';
|
||||||
|
|
||||||
import { BLOCK_KINDS } from '@stores/constants';
|
import { widgetKinds } from '@stores/constants';
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
import { compactNumber } from '@utils/number';
|
import { compactNumber } from '@utils/number';
|
||||||
@ -16,6 +16,7 @@ import { compactNumber } from '@utils/number';
|
|||||||
export function NoteMetadata({ id }: { id: string }) {
|
export function NoteMetadata({ id }: { id: string }) {
|
||||||
const setWidget = useWidgets((state) => state.setWidget);
|
const setWidget = useWidgets((state) => state.setWidget);
|
||||||
|
|
||||||
|
const { db } = useStorage();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
const { status, data } = useQuery(
|
const { status, data } = useQuery(
|
||||||
['note-metadata', id],
|
['note-metadata', id],
|
||||||
@ -35,15 +36,6 @@ export function NoteMetadata({ id }: { id: string }) {
|
|||||||
case 1:
|
case 1:
|
||||||
replies += 1;
|
replies += 1;
|
||||||
if (users.length < 3) users.push(event.pubkey);
|
if (users.length < 3) users.push(event.pubkey);
|
||||||
createReplyNote(
|
|
||||||
id,
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.tags,
|
|
||||||
event.content,
|
|
||||||
event.created_at
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 9735: {
|
case 9735: {
|
||||||
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
|
const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
|
||||||
@ -93,7 +85,11 @@ export function NoteMetadata({ id }: { id: string }) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setWidget({ kind: BLOCK_KINDS.thread, title: 'Thread', content: id })
|
setWidget(db, {
|
||||||
|
kind: widgetKinds.thread,
|
||||||
|
title: 'Thread',
|
||||||
|
content: id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className="text-white/50"
|
className="text-white/50"
|
||||||
>
|
>
|
||||||
|
@ -18,11 +18,7 @@ export function Reply({ event, root }: { event: LumeEvent; root?: string }) {
|
|||||||
<div className="w-11 shrink-0" />
|
<div className="w-11 shrink-0" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<NoteContent content={content} />
|
<NoteContent content={content} />
|
||||||
<NoteActions
|
<NoteActions id={event.id || event.id} pubkey={event.pubkey} root={root} />
|
||||||
id={event.event_id || event.id}
|
|
||||||
pubkey={event.pubkey}
|
|
||||||
root={root}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -16,7 +16,7 @@ export function SubReply({ event }: { event: LumeEvent }) {
|
|||||||
<div className="w-11 shrink-0" />
|
<div className="w-11 shrink-0" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<NoteContent content={content} />
|
<NoteContent content={content} />
|
||||||
<NoteActions id={event.event_id || event.id} pubkey={event.pubkey} />
|
<NoteActions id={event.id || event.id} pubkey={event.pubkey} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { Navigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
|
||||||
|
|
||||||
import { useStronghold } from '@stores/stronghold';
|
|
||||||
|
|
||||||
import { useAccount } from '@utils/hooks/useAccount';
|
|
||||||
|
|
||||||
export function Protected({ children }: { children: ReactNode }) {
|
|
||||||
const privkey = useStronghold((state) => state.privkey);
|
|
||||||
const { status, account } = useAccount();
|
|
||||||
|
|
||||||
if (status === 'loading') {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full w-full items-center justify-center bg-black/90">
|
|
||||||
<LoaderIcon className="h-6 w-6 animate-spin text-white" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return <Navigate to="/auth/welcome" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account && account.privkey.length > 35) {
|
|
||||||
return <Navigate to="/auth/migrate" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account && !privkey) {
|
|
||||||
return <Navigate to="/auth/unlock" replace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
@ -1,8 +1,11 @@
|
|||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { CancelIcon } from '@shared/icons';
|
import { CancelIcon } from '@shared/icons';
|
||||||
|
|
||||||
import { useWidgets } from '@stores/widgets';
|
import { useWidgets } from '@stores/widgets';
|
||||||
|
|
||||||
export function TitleBar({ id, title }: { id?: string; title: string }) {
|
export function TitleBar({ id, title }: { id?: string; title: string }) {
|
||||||
|
const { db } = useStorage();
|
||||||
const remove = useWidgets((state) => state.removeWidget);
|
const remove = useWidgets((state) => state.removeWidget);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -15,7 +18,7 @@ export function TitleBar({ id, title }: { id?: string; title: string }) {
|
|||||||
{id ? (
|
{id ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => remove(id)}
|
onClick={() => remove(db, id)}
|
||||||
className="inline-flex h-6 w-6 shrink translate-y-8 transform items-center justify-center rounded transition-transform duration-150 ease-in-out hover:bg-white/10 group-hover:translate-y-0"
|
className="inline-flex h-6 w-6 shrink translate-y-8 transform items-center justify-center rounded transition-transform duration-150 ease-in-out hover:bg-white/10 group-hover:translate-y-0"
|
||||||
>
|
>
|
||||||
<CancelIcon className="h-3 w-3 text-white" />
|
<CancelIcon className="h-3 w-3 text-white" />
|
||||||
|
@ -8,7 +8,7 @@ import { DEFAULT_AVATAR } from '@stores/constants';
|
|||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
import { formatCreatedAt } from '@utils/createdAt';
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
import { displayNpub, shortenKey } from '@utils/shortenKey';
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
export function User({
|
export function User({
|
||||||
pubkey,
|
pubkey,
|
||||||
@ -85,7 +85,7 @@ export function User({
|
|||||||
{user?.nip05?.toLowerCase() ||
|
{user?.nip05?.toLowerCase() ||
|
||||||
user?.name ||
|
user?.name ||
|
||||||
user?.display_name ||
|
user?.display_name ||
|
||||||
shortenKey(pubkey)}
|
displayNpub(pubkey, 16)}
|
||||||
</h5>
|
</h5>
|
||||||
<span className="leading-none text-white/50">·</span>
|
<span className="leading-none text-white/50">·</span>
|
||||||
<span className="leading-none text-white/50">{createdAt}</span>
|
<span className="leading-none text-white/50">{createdAt}</span>
|
||||||
|
@ -1,92 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
import { immer } from 'zustand/middleware/immer';
|
|
||||||
|
|
||||||
import { createChannelMessage, getChannelMessages, getChannels } from '@libs/storage';
|
|
||||||
|
|
||||||
import { LumeEvent } from '@utils/types';
|
|
||||||
|
|
||||||
export const useChannels = create(
|
|
||||||
immer((set) => ({
|
|
||||||
channels: [],
|
|
||||||
fetch: async () => {
|
|
||||||
const response = await getChannels();
|
|
||||||
set({ channels: response });
|
|
||||||
},
|
|
||||||
add: (event) => {
|
|
||||||
set((state) => {
|
|
||||||
const target = state.channels.findIndex(
|
|
||||||
(m: { event_id: string }) => m.event_id === event.id
|
|
||||||
);
|
|
||||||
if (target !== -1) {
|
|
||||||
state.channels[target]['new_messages'] =
|
|
||||||
state.channels[target]['new_messages'] + 1 || 1;
|
|
||||||
} else {
|
|
||||||
state.channels.push({ event_id: event.id, ...event });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
clearBubble: (id: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const target = state.channels.findIndex(
|
|
||||||
(m: { event_id: string }) => m.event_id === id
|
|
||||||
);
|
|
||||||
state.channels[target]['new_messages'] = 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const useChannelMessages = create(
|
|
||||||
immer((set) => ({
|
|
||||||
messages: [],
|
|
||||||
replyTo: { id: null, pubkey: null, content: null },
|
|
||||||
fetch: async (id: string) => {
|
|
||||||
const events = await getChannelMessages(id);
|
|
||||||
set({ messages: events });
|
|
||||||
},
|
|
||||||
add: (id, event: LumeEvent) => {
|
|
||||||
set((state: any) => {
|
|
||||||
createChannelMessage(
|
|
||||||
id,
|
|
||||||
event.id,
|
|
||||||
event.pubkey,
|
|
||||||
event.kind,
|
|
||||||
event.content,
|
|
||||||
event.tags,
|
|
||||||
event.created_at
|
|
||||||
);
|
|
||||||
state.messages.push({
|
|
||||||
event_id: event.id,
|
|
||||||
channel_id: id,
|
|
||||||
hide: 0,
|
|
||||||
mute: 0,
|
|
||||||
...event,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openReply: (id: string, pubkey: string, content: string) => {
|
|
||||||
set(() => ({ replyTo: { id, pubkey, content } }));
|
|
||||||
},
|
|
||||||
closeReply: () => {
|
|
||||||
set(() => ({ replyTo: { id: null, pubkey: null, content: null } }));
|
|
||||||
},
|
|
||||||
hideMessage: (id: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const target = state.messages.findIndex((m) => m.id === id);
|
|
||||||
state.messages[target]['hide'] = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
muteUser: (pubkey: string) => {
|
|
||||||
set((state) => {
|
|
||||||
const target = state.messages.findIndex((m) => m.pubkey === pubkey);
|
|
||||||
state.messages[target]['mute'] = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
clear: () => {
|
|
||||||
set(() => ({
|
|
||||||
messages: [],
|
|
||||||
replyTo: { id: null, pubkey: null, content: null },
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
);
|
|
@ -1,76 +1,12 @@
|
|||||||
export const APP_VERSION = '1.2.0';
|
|
||||||
|
|
||||||
export const DEFAULT_AVATAR = 'https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih';
|
export const DEFAULT_AVATAR = 'https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih';
|
||||||
|
|
||||||
export const OPENGRAPH = {
|
|
||||||
REGEX_VALID_URL: new RegExp(
|
|
||||||
'^' +
|
|
||||||
// protocol identifier
|
|
||||||
'(?:(?:https?|ftp)://)' +
|
|
||||||
// user:pass authentication
|
|
||||||
'(?:\\S+(?::\\S*)?@)?' +
|
|
||||||
'(?:' +
|
|
||||||
// IP address exclusion
|
|
||||||
// private & local networks
|
|
||||||
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
|
|
||||||
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
|
|
||||||
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
|
||||||
// IP address dotted notation octets
|
|
||||||
// excludes loopback network 0.0.0.0
|
|
||||||
// excludes reserved space >= 224.0.0.0
|
|
||||||
// excludes network & broacast addresses
|
|
||||||
// (first & last IP address of each class)
|
|
||||||
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
|
|
||||||
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
|
|
||||||
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
|
|
||||||
'|' +
|
|
||||||
// host name
|
|
||||||
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
|
||||||
// domain name
|
|
||||||
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
|
|
||||||
// TLD identifier
|
|
||||||
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
|
|
||||||
// TLD may end with dot
|
|
||||||
'\\.?' +
|
|
||||||
')' +
|
|
||||||
// port number
|
|
||||||
'(?::\\d{2,5})?' +
|
|
||||||
// resource path
|
|
||||||
'(?:[/?#]\\S*)?' +
|
|
||||||
'$',
|
|
||||||
'i'
|
|
||||||
),
|
|
||||||
|
|
||||||
REGEX_LOOPBACK: new RegExp(
|
|
||||||
'^' +
|
|
||||||
'(?:(?:10|127)(?:\\.\\d{1,3}){3})' +
|
|
||||||
'|' +
|
|
||||||
'(?:(?:169\\.254|192\\.168|192\\.0)(?:\\.\\d{1,3}){2})' +
|
|
||||||
'|' +
|
|
||||||
'(?:172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
|
||||||
'$',
|
|
||||||
'i'
|
|
||||||
),
|
|
||||||
|
|
||||||
REGEX_CONTENT_TYPE_IMAGE: new RegExp('image/.*', 'i'),
|
|
||||||
|
|
||||||
REGEX_CONTENT_TYPE_AUDIO: new RegExp('audio/.*', 'i'),
|
|
||||||
|
|
||||||
REGEX_CONTENT_TYPE_VIDEO: new RegExp('video/.*', 'i'),
|
|
||||||
|
|
||||||
REGEX_CONTENT_TYPE_TEXT: new RegExp('text/.*', 'i'),
|
|
||||||
|
|
||||||
REGEX_CONTENT_TYPE_APPLICATION: new RegExp('application/.*', 'i'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FULL_RELAYS = [
|
export const FULL_RELAYS = [
|
||||||
'wss://relayable.org',
|
|
||||||
'wss://relay.damus.io',
|
'wss://relay.damus.io',
|
||||||
'wss://relay.nostr.band/all',
|
'wss://relay.nostr.band/all',
|
||||||
'wss://nostr.mutinywallet.com',
|
'wss://nostr.mutinywallet.com',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BLOCK_KINDS = {
|
export const widgetKinds = {
|
||||||
image: 0,
|
image: 0,
|
||||||
feed: 1,
|
feed: 1,
|
||||||
thread: 2,
|
thread: 2,
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export const COMPOSE_SHORTCUT = 'meta+n';
|
|
||||||
export const ADD_IMAGEBLOCK_SHORTCUT = 'meta+i';
|
|
||||||
export const ADD_FEEDBLOCK_SHORTCUT = 'meta+f';
|
|
||||||
export const ADD_HASHTAGBLOCK_SHORTCUT = 'meta+t';
|
|
@ -18,6 +18,15 @@ export const useWidgets = create<WidgetState>()(
|
|||||||
widgets: null,
|
widgets: null,
|
||||||
fetchWidgets: async (db: LumeStorage) => {
|
fetchWidgets: async (db: LumeStorage) => {
|
||||||
const widgets = await db.getWidgets();
|
const widgets = await db.getWidgets();
|
||||||
|
|
||||||
|
// default: add network widget
|
||||||
|
widgets.unshift({
|
||||||
|
id: String(widgets.length + 1),
|
||||||
|
title: 'Network',
|
||||||
|
content: '',
|
||||||
|
kind: 9999,
|
||||||
|
});
|
||||||
|
|
||||||
set({ widgets: widgets });
|
set({ widgets: widgets });
|
||||||
},
|
},
|
||||||
setWidget: async (db: LumeStorage, { kind, title, content }: Widget) => {
|
setWidget: async (db: LumeStorage, { kind, title, content }: Widget) => {
|
||||||
|
@ -1,25 +1,2 @@
|
|||||||
// get X days ago with user provided date
|
|
||||||
export function getDayAgo(numOfDays, date = new Date()) {
|
|
||||||
const days = new Date(date.getTime());
|
|
||||||
days.setDate(date.getDate() - numOfDays);
|
|
||||||
|
|
||||||
return days;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get X hours ago with user provided date
|
|
||||||
export function getHourAgo(numOfHours, date = new Date()) {
|
|
||||||
const hours = new Date(date.getTime());
|
|
||||||
hours.setHours(date.getHours() - numOfHours);
|
|
||||||
|
|
||||||
return hours;
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert date to unix timestamp
|
|
||||||
export function dateToUnix(_date?: Date) {
|
|
||||||
const date = _date || new Date();
|
|
||||||
|
|
||||||
return Math.floor(date.getTime() / 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nHoursAgo = (hrs: number): number =>
|
export const nHoursAgo = (hrs: number): number =>
|
||||||
Math.floor((Date.now() - hrs * 60 * 60 * 1000) / 1000);
|
Math.floor((Date.now() - hrs * 60 * 60 * 1000) / 1000);
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { useNDK } from '@libs/ndk/provider';
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
export function useAccount() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { ndk } = useNDK();
|
|
||||||
const { status, data: account } = useQuery(
|
|
||||||
['account'],
|
|
||||||
async () => {
|
|
||||||
const account = await db.getActiveAccount();
|
|
||||||
console.log('account: ', account);
|
|
||||||
if (account?.pubkey) {
|
|
||||||
const user = ndk.getUser({ hexpubkey: account?.pubkey });
|
|
||||||
await user.fetchProfile();
|
|
||||||
return { ...account, ...user.profile };
|
|
||||||
}
|
|
||||||
return account;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!ndk,
|
|
||||||
staleTime: Infinity,
|
|
||||||
refetchOnMount: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
refetchOnReconnect: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return { status, account };
|
|
||||||
}
|
|
@ -16,7 +16,7 @@ export function useEvent(id: string, embed?: string) {
|
|||||||
return embed as unknown as LumeEvent;
|
return embed as unknown as LumeEvent;
|
||||||
} else {
|
} else {
|
||||||
const event = (await ndk.fetchEvent(id)) as LumeEvent;
|
const event = (await ndk.fetchEvent(id)) as LumeEvent;
|
||||||
if (!event) return null;
|
if (!event) throw new Error('event not found');
|
||||||
if (event.kind === 1) event['content'] = parser(event) as unknown as string;
|
if (event.kind === 1) event['content'] = parser(event) as unknown as string;
|
||||||
return event as LumeEvent;
|
return event as LumeEvent;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
NDKSubscription,
|
NDKSubscription,
|
||||||
NDKUser,
|
NDKUser,
|
||||||
} from '@nostr-dev-kit/ndk';
|
} from '@nostr-dev-kit/ndk';
|
||||||
import { destr } from 'destr';
|
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import { nip19 } from 'nostr-tools';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@ -17,6 +16,14 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
import { useStronghold } from '@stores/stronghold';
|
import { useStronghold } from '@stores/stronghold';
|
||||||
|
|
||||||
import { nHoursAgo } from '@utils/date';
|
import { nHoursAgo } from '@utils/date';
|
||||||
|
import { LumeEvent } from '@utils/types';
|
||||||
|
|
||||||
|
interface NotesResponse {
|
||||||
|
status: string;
|
||||||
|
data: LumeEvent[];
|
||||||
|
nextCursor?: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function useNostr() {
|
export function useNostr() {
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
@ -37,6 +44,8 @@ export function useNostr() {
|
|||||||
callback: (event: NDKEvent) => void,
|
callback: (event: NDKEvent) => void,
|
||||||
closeOnEose?: boolean
|
closeOnEose?: boolean
|
||||||
) => {
|
) => {
|
||||||
|
if (!ndk) throw new Error('NDK instance not found');
|
||||||
|
|
||||||
const subEvent = ndk.subscribe(filter, { closeOnEose: closeOnEose ?? true });
|
const subEvent = ndk.subscribe(filter, { closeOnEose: closeOnEose ?? true });
|
||||||
subManager.set(JSON.stringify(filter), subEvent);
|
subManager.set(JSON.stringify(filter), subEvent);
|
||||||
|
|
||||||
@ -78,17 +87,22 @@ export function useNostr() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchNotes = async (since: number) => {
|
const fetchNotes = async (since: number): Promise<NotesResponse> => {
|
||||||
try {
|
try {
|
||||||
if (!ndk) return { status: 'failed', message: 'NDK instance not found' };
|
if (!ndk) return { status: 'failed', data: [], message: 'NDK instance not found' };
|
||||||
|
|
||||||
|
console.log('fetch all events since: ', since);
|
||||||
const events = await ndk.fetchEvents({
|
const events = await ndk.fetchEvents({
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
authors: db.account.network ?? db.account.follows,
|
authors: db.account.network ?? db.account.follows,
|
||||||
since: nHoursAgo(since),
|
since: nHoursAgo(since),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { status: 'ok', data: [...events], nextCursor: since * 2 };
|
const sorted = [...events].sort(
|
||||||
|
(a, b) => b.created_at - a.created_at
|
||||||
|
) as unknown as LumeEvent[];
|
||||||
|
|
||||||
|
return { status: 'ok', data: sorted, nextCursor: since * 2 };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('failed get notes, error: ', e);
|
console.error('failed get notes, error: ', e);
|
||||||
return { status: 'failed', data: [], message: e };
|
return { status: 'failed', data: [], message: e };
|
||||||
@ -122,14 +136,6 @@ export function useNostr() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createZap = async (event: NDKEvent, amount: number, message?: string) => {
|
const createZap = async (event: NDKEvent, amount: number, message?: string) => {
|
||||||
// @ts-expect-error, LumeEvent to NDKEvent
|
|
||||||
event.id = event.event_id;
|
|
||||||
|
|
||||||
// @ts-expect-error, LumeEvent to NDKEvent
|
|
||||||
if (typeof event.content !== 'string') event.content = event.content.original;
|
|
||||||
|
|
||||||
if (typeof event.tags === 'string') event.tags = destr(event.tags);
|
|
||||||
|
|
||||||
if (!privkey) throw new Error('Private key not found');
|
if (!privkey) throw new Error('Private key not found');
|
||||||
|
|
||||||
if (!ndk.signer) {
|
if (!ndk.signer) {
|
||||||
@ -137,7 +143,7 @@ export function useNostr() {
|
|||||||
ndk.signer = signer;
|
ndk.signer = signer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error, LumeEvent to NDKEvent
|
// @ts-expect-error, NostrEvent to NDKEvent
|
||||||
const ndkEvent = new NDKEvent(ndk, event);
|
const ndkEvent = new NDKEvent(ndk, event);
|
||||||
const res = await ndkEvent.zap(amount, message ?? 'zap from lume');
|
const res = await ndkEvent.zap(amount, message ?? 'zap from lume');
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { invoke } from '@tauri-apps/api/tauri';
|
|||||||
import { Opengraph } from '@utils/types';
|
import { Opengraph } from '@utils/types';
|
||||||
|
|
||||||
export function useOpenGraph(url: string) {
|
export function useOpenGraph(url: string) {
|
||||||
const { status, data, error, isFetching } = useQuery(
|
const { status, data, error } = useQuery(
|
||||||
['preview', url],
|
['preview', url],
|
||||||
async () => {
|
async () => {
|
||||||
const res: Opengraph = await invoke('opengraph', { url });
|
const res: Opengraph = await invoke('opengraph', { url });
|
||||||
@ -25,6 +25,5 @@ export function useOpenGraph(url: string) {
|
|||||||
status,
|
status,
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
isFetching,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
import { BaseDirectory, appConfigDir } from '@tauri-apps/api/path';
|
|
||||||
import { removeFile } from '@tauri-apps/plugin-fs';
|
|
||||||
import { Stronghold } from '@tauri-apps/plugin-stronghold';
|
|
||||||
|
|
||||||
const dir = await appConfigDir();
|
|
||||||
|
|
||||||
export function useSecureStorage() {
|
|
||||||
async function getClient(stronghold: Stronghold) {
|
|
||||||
try {
|
|
||||||
return await stronghold.loadClient('lume');
|
|
||||||
} catch {
|
|
||||||
return await stronghold.createClient('lume');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async (key: string, value: string, password: string) => {
|
|
||||||
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, password);
|
|
||||||
const client = await getClient(stronghold);
|
|
||||||
const store = client.getStore();
|
|
||||||
await store.insert(key, Array.from(new TextEncoder().encode(value)));
|
|
||||||
return await stronghold.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
const load = async (key: string, password: string) => {
|
|
||||||
const stronghold = await Stronghold.load(`${dir}/lume.stronghold`, password);
|
|
||||||
const client = await getClient(stronghold);
|
|
||||||
const store = client.getStore();
|
|
||||||
const value = await store.get(key);
|
|
||||||
const decoded = new TextDecoder().decode(new Uint8Array(value));
|
|
||||||
return decoded;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = async () => {
|
|
||||||
return await removeFile('lume.stronghold', { dir: BaseDirectory.AppConfig });
|
|
||||||
};
|
|
||||||
|
|
||||||
return { save, load, reset };
|
|
||||||
}
|
|
@ -1,30 +1,5 @@
|
|||||||
import { NDKTag } from '@nostr-dev-kit/ndk';
|
import { NDKTag } from '@nostr-dev-kit/ndk';
|
||||||
import { destr } from 'destr';
|
import { destr } from 'destr';
|
||||||
import { nip19 } from 'nostr-tools';
|
|
||||||
|
|
||||||
export function truncateContent(str, n) {
|
|
||||||
return str.length > n ? `${str.slice(0, n - 1)}...` : str;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setToArray(tags: any) {
|
|
||||||
const newArray = [];
|
|
||||||
tags.forEach((item) => {
|
|
||||||
const hexpubkey = nip19.decode(item.npub).data;
|
|
||||||
newArray.push(hexpubkey);
|
|
||||||
});
|
|
||||||
|
|
||||||
return newArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert NIP-02 to array of pubkey
|
|
||||||
export function nip02ToArray(tags: any) {
|
|
||||||
const arr = [];
|
|
||||||
tags.forEach((item) => {
|
|
||||||
arr.push(item[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert array to NIP-02 tag list
|
// convert array to NIP-02 tag list
|
||||||
export function arrayToNIP02(arr: string[]) {
|
export function arrayToNIP02(arr: string[]) {
|
||||||
@ -36,52 +11,9 @@ export function arrayToNIP02(arr: string[]) {
|
|||||||
return nip02_arr;
|
return nip02_arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert array object to pure array
|
// get repost id from event tags
|
||||||
export function arrayObjToPureArr(arr: any) {
|
|
||||||
const pure_arr = [];
|
|
||||||
arr.forEach((item) => {
|
|
||||||
pure_arr.push(item.content);
|
|
||||||
});
|
|
||||||
|
|
||||||
return pure_arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get parent id from event tags
|
|
||||||
export function getParentID(arr: string[][], fallback: string) {
|
|
||||||
const tags = destr(arr);
|
|
||||||
let parentID = fallback;
|
|
||||||
|
|
||||||
if (tags.length > 0) {
|
|
||||||
if (tags[0][0] === 'e') {
|
|
||||||
parentID = tags[0][1];
|
|
||||||
} else {
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
if (tag[0] === 'e' && (tag[2] === 'root' || tag[3] === 'root')) {
|
|
||||||
parentID = tag[1];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parentID;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check id present in event tags
|
|
||||||
export function isTagsIncludeID(id: string, arr: NDKTag[]) {
|
|
||||||
const tags = destr(arr);
|
|
||||||
|
|
||||||
if (tags.length > 0) {
|
|
||||||
if (tags[0][1] === id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get parent id from event tags
|
|
||||||
export function getRepostID(arr: NDKTag[]) {
|
export function getRepostID(arr: NDKTag[]) {
|
||||||
const tags = destr(arr);
|
const tags = destr(arr) as string[];
|
||||||
let quoteID = null;
|
let quoteID = null;
|
||||||
|
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
@ -94,12 +26,3 @@ export function getRepostID(arr: NDKTag[]) {
|
|||||||
|
|
||||||
return quoteID;
|
return quoteID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort events by timestamp
|
|
||||||
export function sortEvents(arr: any) {
|
|
||||||
arr.sort((a, b) => {
|
|
||||||
return a.created_at - b.created_at;
|
|
||||||
});
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
@ -1,19 +1,9 @@
|
|||||||
import react from '@vitejs/plugin-react-swc';
|
import react from '@vitejs/plugin-react-swc';
|
||||||
// import million from 'million/compiler';
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import topLevelAwait from 'vite-plugin-top-level-await';
|
|
||||||
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
import viteTsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [react(), viteTsconfigPaths()],
|
||||||
// million.vite(),
|
|
||||||
react(),
|
|
||||||
viteTsconfigPaths(),
|
|
||||||
topLevelAwait({
|
|
||||||
promiseExportName: '__tla',
|
|
||||||
promiseImportName: (i) => `__tla_${i}`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
envPrefix: ['VITE_', 'TAURI_'],
|
envPrefix: ['VITE_', 'TAURI_'],
|
||||||
build: {
|
build: {
|
||||||
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
|
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
|
||||||
|
Loading…
Reference in New Issue
Block a user