This commit is contained in:
Ren Amamiya 2023-09-26 09:05:39 +07:00
parent a66770989b
commit 236131087a
21 changed files with 605 additions and 523 deletions

View File

@ -19,14 +19,15 @@
}, },
"dependencies": { "dependencies": {
"@getalby/sdk": "^2.4.0", "@getalby/sdk": "^2.4.0",
"@nostr-dev-kit/ndk": "^1.2.1", "@nostr-dev-kit/ndk": "^1.3.0",
"@nostr-fetch/adapter-ndk": "^0.12.2", "@nostr-fetch/adapter-ndk": "^0.12.2",
"@radix-ui/react-alert-dialog": "^1.0.4", "@radix-ui/react-alert-dialog": "^1.0.5",
"@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.5",
"@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.6", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^4.35.3", "@tanstack/react-query": "^4.35.3",
"@tanstack/react-virtual": "3.0.0-beta.54", "@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.4.0", "@tauri-apps/api": "^1.4.0",
@ -50,20 +51,19 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-currency-input-field": "^3.6.11", "react-currency-input-field": "^3.6.11",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.46.1", "react-hook-form": "^7.46.2",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-organizational-chart": "^2.2.1",
"react-player": "^2.13.0", "react-player": "^2.13.0",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.16.0",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
"react-virtuoso": "^4.6.0", "react-virtuoso": "^4.6.0",
"react-zoom-pan-pinch": "^3.1.0",
"reactflow": "^11.8.3", "reactflow": "^11.8.3",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql#v1", "tauri-plugin-sql-api": "github:tauri-apps/tauri-plugin-sql#v1",
"tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1", "tauri-plugin-store-api": "github:tauri-apps/tauri-plugin-store#v1",
"tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1", "tauri-plugin-stronghold-api": "github:tauri-apps/tauri-plugin-stronghold#v1",
"tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1", "tauri-plugin-upload-api": "github:tauri-apps/tauri-plugin-upload#v1",
"tippy.js": "^6.3.7",
"zustand": "^4.4.1" "zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
@ -71,19 +71,19 @@
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0", "@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@types/html-to-text": "^9.0.2", "@types/html-to-text": "^9.0.2",
"@types/node": "^20.6.3", "@types/node": "^20.7.0",
"@types/react": "^18.2.22", "@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@types/youtube-player": "^5.5.7", "@types/youtube-player": "^5.5.8",
"@typescript-eslint/eslint-plugin": "^6.7.2", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.2", "@typescript-eslint/parser": "^6.7.3",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.4.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"csstype": "^3.1.2", "csstype": "^3.1.2",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",

File diff suppressed because it is too large Load Diff

View File

@ -90,7 +90,7 @@ export const UserWithDrawer = memo(function UserWithDrawer({
{displayNpub(pubkey, 16)} {displayNpub(pubkey, 16)}
</span> </span>
)} )}
{user.about ? <TextNote content={user.about} /> : null} {user?.about ? <TextNote content={user?.about} /> : null}
</div> </div>
<div className="mt-3 inline-flex items-center gap-2"> <div className="mt-3 inline-flex items-center gap-2">
{followed ? ( {followed ? (

View File

@ -38,7 +38,10 @@ export function ChatsListItem({ pubkey }: { pubkey: string }) {
/> />
<div className="inline-flex w-full flex-1 items-center justify-between"> <div className="inline-flex w-full flex-1 items-center justify-between">
<h5 className="max-w-[10rem] truncate"> <h5 className="max-w-[10rem] truncate">
{user?.name || user?.display_name || displayNpub(pubkey, 16)} {user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</h5> </h5>
</div> </div>
</NavLink> </NavLink>

View File

@ -22,7 +22,7 @@ export function ChatSidebar({ pubkey }: { pubkey: string }) {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h3 className="text-lg font-semibold leading-none"> <h3 className="text-lg font-semibold leading-none">
{user?.display_name || user?.name} {user?.name || user?.display_name || user?.displayName}
</h3> </h3>
{user?.nip05 ? ( {user?.nip05 ? (
<NIP05 <NIP05

View File

@ -25,7 +25,7 @@ export function NotiUser({ pubkey }: { pubkey: string }) {
className="h-8 w-8 shrink-0 rounded-md object-cover" className="h-8 w-8 shrink-0 rounded-md object-cover"
/> />
<span className="max-w-[10rem] truncate font-medium leading-none text-white"> <span className="max-w-[10rem] truncate font-medium leading-none text-white">
{user?.name || user?.display_name || displayNpub(pubkey, 16)} {user?.name || user?.display_name || user?.displayName || displayNpub(pubkey, 16)}
</span> </span>
</div> </div>
); );

View File

@ -71,7 +71,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
<div className="inline-flex flex-col items-center gap-1.5"> <div className="inline-flex flex-col items-center gap-1.5">
<h5 className="text-center text-xl font-semibold leading-none"> <h5 className="text-center text-xl font-semibold leading-none">
{user.display_name || user.displayName || user.name || 'No name'} {user.name || user.display_name || user.displayName || 'No name'}
</h5> </h5>
{user.nip05 ? ( {user.nip05 ? (
<NIP05 <NIP05

View File

@ -5,6 +5,7 @@ import { fetch } from '@tauri-apps/api/http';
import { NostrFetcher } from 'nostr-fetch'; import { NostrFetcher } from 'nostr-fetch';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import TauriAdapter from '@libs/ndk/cache';
import { useStorage } from '@libs/storage/provider'; import { useStorage } from '@libs/storage/provider';
export const NDKInstance = () => { export const NDKInstance = () => {
@ -13,6 +14,7 @@ export const NDKInstance = () => {
const [ndk, setNDK] = useState<NDK | undefined>(undefined); const [ndk, setNDK] = useState<NDK | undefined>(undefined);
const [relayUrls, setRelayUrls] = useState<string[]>([]); const [relayUrls, setRelayUrls] = useState<string[]>([]);
const cacheAdapter = useMemo(() => new TauriAdapter(), []);
const fetcher = useMemo( const fetcher = useMemo(
() => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null), () => (ndk ? NostrFetcher.withCustomPool(ndkAdapter(ndk)) : null),
[ndk] [ndk]
@ -58,6 +60,7 @@ export const NDKInstance = () => {
const explicitRelayUrls = await getExplicitRelays(); const explicitRelayUrls = await getExplicitRelays();
const instance = new NDK({ const instance = new NDK({
explicitRelayUrls, explicitRelayUrls,
cacheAdapter,
}); });
try { try {
@ -75,6 +78,10 @@ export const NDKInstance = () => {
useEffect(() => { useEffect(() => {
if (!ndk) initNDK(); if (!ndk) initNDK();
return () => {
cacheAdapter.saveCache();
};
}, []); }, []);
return { return {

View File

@ -1,4 +1,4 @@
import { NDKEvent } from '@nostr-dev-kit/ndk'; import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
import { BaseDirectory, removeFile } from '@tauri-apps/api/fs'; import { BaseDirectory, removeFile } from '@tauri-apps/api/fs';
import Database from 'tauri-plugin-sql-api'; import Database from 'tauri-plugin-sql-api';
import { Stronghold } from 'tauri-plugin-stronghold-api'; import { Stronghold } from 'tauri-plugin-stronghold-api';
@ -302,6 +302,13 @@ export class LumeStorage {
); );
} }
public async createProfile(pubkey: string, profile: NDKUserProfile) {
return await this.db.execute(
'INSERT OR REPLACE INTO metadata (id, event, author, kind, created_at) VALUES ($1, $2, $3, $4, $5);',
[pubkey, JSON.stringify(profile), pubkey, 0, Math.round(Date.now() / 1000)]
);
}
public async getMetadataByPubkey(pubkey: string) { public async getMetadataByPubkey(pubkey: string) {
const results: DBEvent[] = await this.db.select( const results: DBEvent[] = await this.db.select(
'SELECT * FROM metadata WHERE author = $1 AND kind = "0" LIMIT 1;', 'SELECT * FROM metadata WHERE author = $1 AND kind = "0" LIMIT 1;',

View File

@ -35,7 +35,9 @@ export function ActiveAccount() {
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
}; };
sub(filter, async (event) => { sub(
filter,
async (event) => {
addActivity(event); addActivity(event);
switch (event.kind) { switch (event.kind) {
@ -52,7 +54,9 @@ export function ActiveAccount() {
default: default:
break; break;
} }
}); },
false
);
}, []); }, []);
if (status === 'loading') { if (status === 'loading') {
@ -74,7 +78,7 @@ export function ActiveAccount() {
/> />
<div className="flex w-full flex-1 flex-col items-start gap-0.5"> <div className="flex w-full flex-1 flex-col items-start gap-0.5">
<p className="max-w-[10rem] truncate font-semibold leading-none text-white"> <p className="max-w-[10rem] truncate font-semibold leading-none text-white">
{user?.name || user?.display_name} {user?.name || user?.display_name || user?.displayName}
</p> </p>
<span className="max-w-[7rem] truncate text-sm leading-none text-white/50"> <span className="max-w-[7rem] truncate text-sm leading-none text-white/50">
{user?.nip05 || displayNpub(db.account.pubkey, 12)} {user?.nip05 || displayNpub(db.account.pubkey, 12)}

View File

@ -3,4 +3,6 @@ export * from './modal';
export * from './composer'; export * from './composer';
export * from './mention/item'; export * from './mention/item';
export * from './mention/popup'; export * from './mention/popup';
export * from './mention/suggestion';
export * from './mention/inlineList';
export * from './mediaUploader'; export * from './mediaUploader';

View File

@ -0,0 +1,83 @@
import { type SuggestionProps } from '@tiptap/suggestion';
import {
ForwardedRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { twMerge } from 'tailwind-merge';
import { MentionItem } from '@shared/composer';
export const MentionInlineList = forwardRef(
(props: SuggestionProps, ref: ForwardedRef<unknown>) => {
const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = (index) => {
const item = props.items[index];
if (item) {
props.command({ id: item });
}
};
const upHandler = () => {
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
};
const downHandler = () => {
setSelectedIndex((selectedIndex + 1) % props.items.length);
};
const enterHandler = () => {
selectItem(selectedIndex);
};
useEffect(() => setSelectedIndex(0), [props.items]);
useImperativeHandle(ref, () => ({
onKeyDown: ({ event }) => {
if (event.key === 'ArrowUp') {
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
downHandler();
return true;
}
if (event.key === 'Enter') {
enterHandler();
return true;
}
return false;
},
}));
return (
<div className="flex w-[250px] flex-col rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
{props.items.length ? (
props.items.map((item: string, index: number) => (
<button
className={twMerge(
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-white/10',
`${index === selectedIndex ? 'is-selected' : ''}`
)}
key={index}
onClick={() => selectItem(index)}
>
<MentionItem embed={item} />
</button>
))
) : (
<div>No result</div>
)}
</div>
);
}
);
MentionInlineList.displayName = 'MentionList';

View File

@ -3,12 +3,12 @@ import { Image } from '@shared/image';
import { useProfile } from '@utils/hooks/useProfile'; import { useProfile } from '@utils/hooks/useProfile';
import { displayNpub } from '@utils/shortenKey'; import { displayNpub } from '@utils/shortenKey';
export function MentionItem({ pubkey }: { pubkey: string }) { export function MentionItem({ pubkey, embed }: { pubkey: string; embed?: string }) {
const { status, user } = useProfile(pubkey); const { status, user } = useProfile(pubkey, embed);
if (status === 'loading') { if (status === 'loading') {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2.5 px-2">
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" /> <div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start"> <div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10 backdrop-blur-xl" /> <span className="h-4 w-1/2 animate-pulse rounded bg-white/10 backdrop-blur-xl" />

View File

@ -0,0 +1,68 @@
import { ReactRenderer } from '@tiptap/react';
import tippy from 'tippy.js';
import { MentionInlineList } from '@shared/composer';
export const Suggestion = {
items: async ({ query }) => {
const users = [];
return users
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
},
render: () => {
let component;
let popup;
return {
onStart: (props) => {
component = new ReactRenderer(MentionInlineList, {
props,
editor: props.editor,
});
if (!props.clientRect) {
return;
}
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
});
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) {
return;
}
popup[0].setProps({
getReferenceClientRect: props.clientRect,
});
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup[0].hide();
return true;
}
return component.ref?.onKeyDown(props);
},
onExit() {
popup[0].destroy();
component.destroy();
},
};
},
};

View File

@ -14,7 +14,7 @@ export function ComposerUser({ pubkey }: { pubkey: string }) {
className="h-10 w-10 shrink-0 rounded-lg" className="h-10 w-10 shrink-0 rounded-lg"
/> />
<h5 className="text-base font-semibold leading-none text-white"> <h5 className="text-base font-semibold leading-none text-white">
{user?.name || user?.display_name || displayNpub(pubkey, 16)} {user?.name || user?.display_name || user?.displayName || displayNpub(pubkey, 16)}
</h5> </h5>
</div> </div>
); );

View File

@ -60,7 +60,7 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
if (send) { if (send) {
await sendNativeNotification( await sendNativeNotification(
`You've tipped ${compactNumber.format(send.amount)} sats to ${ `You've tipped ${compactNumber.format(send.amount)} sats to ${
user?.display_name || user?.name user?.name || user?.display_name || user?.displayName
}` }`
); );
@ -106,7 +106,7 @@ export function NoteZap({ id, pubkey }: { id: string; pubkey: string }) {
<div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3"> <div className="inline-flex w-full shrink-0 items-center justify-between px-5 py-3">
<div className="w-6" /> <div className="w-6" />
<Dialog.Title className="text-center text-sm font-semibold leading-none text-white"> <Dialog.Title className="text-center text-sm font-semibold leading-none text-white">
Send tip to {user?.display_name || user?.name} Send tip to {user?.name || user?.display_name || user?.displayName}
</Dialog.Title> </Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10"> <Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" /> <CancelIcon className="h-4 w-4 text-white/50" />

View File

@ -18,20 +18,24 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
onClick={() => onClick={() =>
setWidget(db, { setWidget(db, {
kind: WidgetKinds.local.user, kind: WidgetKinds.local.user,
title: user?.name || user?.display_name, title: user?.name || user?.display_name || user?.displayName,
content: pubkey, content: pubkey,
}) })
} }
onKeyDown={() => onKeyDown={() =>
setWidget(db, { setWidget(db, {
kind: WidgetKinds.local.user, kind: WidgetKinds.local.user,
title: user?.name || user?.display_name, title: user?.name || user?.display_name || user?.displayName,
content: pubkey, content: pubkey,
}) })
} }
className="break-words text-fuchsia-400 hover:text-fuchsia-500" className="break-words text-fuchsia-400 hover:text-fuchsia-500"
> >
{user?.name || user?.display_name || user?.username || displayNpub(pubkey, 16)} {user?.name ||
user?.display_name ||
user?.displayName ||
user?.username ||
displayNpub(pubkey, 16)}
</span> </span>
); );
} }

View File

@ -1,4 +1,4 @@
import * as Popover from '@radix-ui/react-popover'; import * as HoverCard from '@radix-ui/react-hover-card';
import { memo } from 'react'; import { memo } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -70,7 +70,10 @@ export const User = memo(function User({
</button> </button>
<div className="flex flex-1 items-baseline gap-2"> <div className="flex flex-1 items-baseline gap-2">
<h5 className="max-w-[10rem] truncate font-semibold leading-none text-white"> <h5 className="max-w-[10rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name || displayNpub(pubkey, 16)} {user?.name ||
user?.display_name ||
user?.displayName ||
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>
@ -90,7 +93,7 @@ export const User = memo(function User({
<div className="flex h-full flex-col items-start justify-between"> <div className="flex h-full flex-col items-start justify-between">
<div className="flex flex-col items-start gap-1 text-start"> <div className="flex flex-col items-start gap-1 text-start">
<p className="max-w-[15rem] truncate text-lg font-semibold leading-none text-white"> <p className="max-w-[15rem] truncate text-lg font-semibold leading-none text-white">
{user?.name || user?.display_name} {user?.name || user?.display_name || user?.displayName}
</p> </p>
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
@ -110,7 +113,7 @@ export const User = memo(function User({
className="inline-flex items-center gap-2 text-sm text-white/70" className="inline-flex items-center gap-2 text-sm text-white/70"
> >
<WorldIcon className="h-4 w-4" /> <WorldIcon className="h-4 w-4" />
<p className="max-w-[10rem] truncate">{user.website}</p> <p className="max-w-[10rem] truncate">{user?.website}</p>
</Link> </Link>
) : null} ) : null}
</div> </div>
@ -129,7 +132,7 @@ export const User = memo(function User({
/> />
<div className="flex w-full flex-col gap-1"> <div className="flex w-full flex-col gap-1">
<h3 className="max-w-[15rem] truncate font-medium leading-none text-white"> <h3 className="max-w-[15rem] truncate font-medium leading-none text-white">
{user?.name || user?.display_name} {user?.name || user?.display_name || user?.displayName}
</h3> </h3>
<p className="text-sm leading-none text-white/70"> <p className="text-sm leading-none text-white/70">
{user?.nip05 || user?.username || displayNpub(pubkey, 16)} {user?.nip05 || user?.username || displayNpub(pubkey, 16)}
@ -163,7 +166,10 @@ export const User = memo(function User({
/> />
<div className="inline-flex items-baseline gap-1"> <div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[10rem] truncate font-medium leading-none text-white/80"> <h5 className="max-w-[10rem] truncate font-medium leading-none text-white/80">
{user?.display_name || user?.name || displayNpub(pubkey, 16)} {user?.name ||
user?.display_name ||
user?.displayName ||
displayNpub(pubkey, 16)}
</h5> </h5>
<span className="text-blue-500">reposted</span> <span className="text-blue-500">reposted</span>
</div> </div>
@ -182,7 +188,7 @@ export const User = memo(function User({
/> />
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white"> <h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name} {user?.name || user?.display_name || user?.displayName}
</h5> </h5>
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
<span className="leading-none text-white/50">{createdAt}</span> <span className="leading-none text-white/50">{createdAt}</span>
@ -195,9 +201,9 @@ export const User = memo(function User({
} }
return ( return (
<Popover.Root> <HoverCard.Root>
<div className="relative z-10 flex items-start gap-3"> <div className="relative z-10 flex items-start gap-3">
<Popover.Trigger asChild> <HoverCard.Trigger asChild>
<button <button
type="button" type="button"
className="relative z-40 h-10 w-10 shrink-0 overflow-hidden" className="relative z-40 h-10 w-10 shrink-0 overflow-hidden"
@ -208,18 +214,21 @@ export const User = memo(function User({
className="h-10 w-10 rounded-lg object-cover" className="h-10 w-10 rounded-lg object-cover"
/> />
</button> </button>
</Popover.Trigger> </HoverCard.Trigger>
<div className="flex flex-1 items-baseline gap-2"> <div className="flex flex-1 items-baseline gap-2">
<h5 className="max-w-[15rem] truncate font-semibold leading-none text-white"> <h5 className="max-w-[15rem] truncate font-semibold leading-none text-white">
{user?.display_name || user?.name || displayNpub(pubkey, 16)} {user?.name ||
user?.display_name ||
user?.displayName ||
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>
</div> </div>
</div> </div>
<Popover.Portal> <HoverCard.Portal>
<Popover.Content <HoverCard.Content
className="w-[300px] overflow-hidden rounded-xl border border-white/10 bg-white/10 backdrop-blur-3xl focus:outline-none" className="w-[300px] overflow-hidden rounded-xl border border-white/10 bg-white/10 backdrop-blur-3xl focus:outline-none data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=top]:animate-slideDownAndFade data-[state=open]:transition-all"
sideOffset={5} sideOffset={5}
> >
<div className="flex gap-2.5 border-b border-white/5 px-3 py-3"> <div className="flex gap-2.5 border-b border-white/5 px-3 py-3">
@ -231,12 +240,15 @@ export const User = memo(function User({
<div className="flex flex-1 flex-col gap-2"> <div className="flex flex-1 flex-col gap-2">
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<h5 className="text-sm font-semibold leading-none"> <h5 className="text-sm font-semibold leading-none">
{user?.display_name || user?.name || user?.username} {user?.name ||
user?.display_name ||
user?.displayName ||
user?.username}
</h5> </h5>
{user.nip05 ? ( {user?.nip05 ? (
<NIP05 <NIP05
pubkey={pubkey} pubkey={pubkey}
nip05={user.nip05} nip05={user?.nip05}
className="max-w-[15rem] truncate text-sm leading-none text-white/50" className="max-w-[15rem] truncate text-sm leading-none text-white/50"
/> />
) : ( ) : (
@ -266,8 +278,8 @@ export const User = memo(function User({
Message Message
</Link> </Link>
</div> </div>
</Popover.Content> </HoverCard.Content>
</Popover.Portal> </HoverCard.Portal>
</Popover.Root> </HoverCard.Root>
); );
}); });

View File

@ -14,7 +14,7 @@ export const useSidebar = create<SidebarState>()(
persist( persist(
(set) => ({ (set) => ({
feeds: true, feeds: true,
chats: true, chats: false,
integrations: true, integrations: true,
toggleFeeds: () => set((state) => ({ feeds: !state.feeds })), toggleFeeds: () => set((state) => ({ feeds: !state.feeds })),
toggleChats: () => set((state) => ({ chats: !state.chats })), toggleChats: () => set((state) => ({ chats: !state.chats })),

View File

@ -36,10 +36,17 @@ export function useNostr() {
[] []
); );
const sub = async (filter: NDKFilter, callback: (event: NDKEvent) => void) => { const sub = async (
filter: NDKFilter,
callback: (event: NDKEvent) => void,
groupable?: boolean
) => {
if (!ndk) throw new Error('NDK instance not found'); if (!ndk) throw new Error('NDK instance not found');
const subEvent = ndk.subscribe(filter, { closeOnEose: false }); const subEvent = ndk.subscribe(filter, {
closeOnEose: false,
groupable: groupable ?? true,
});
subManager.set(JSON.stringify(filter), subEvent); subManager.set(JSON.stringify(filter), subEvent);
subEvent.addListener('event', (event: NDKEvent) => { subEvent.addListener('event', (event: NDKEvent) => {

View File

@ -1,4 +1,4 @@
import { NDKKind, NDKUserProfile } from '@nostr-dev-kit/ndk'; import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useNDK } from '@libs/ndk/provider'; import { useNDK } from '@libs/ndk/provider';
@ -14,30 +14,20 @@ export function useProfile(pubkey: string, embed?: string) {
} = useQuery( } = useQuery(
['user', pubkey], ['user', pubkey],
async () => { async () => {
if (!embed) { if (embed) {
const cleanPubkey = pubkey.replace('-', '');
const dbEvent = await db.getMetadataByPubkey(cleanPubkey);
if (dbEvent) {
return JSON.parse(dbEvent.content) as NDKUserProfile;
}
const events = await ndk.fetchEvents(
{
kinds: [NDKKind.Metadata],
authors: [cleanPubkey],
limit: 100,
},
{ closeOnEose: true }
);
if (!events) throw new Error(`User not found: ${pubkey}`);
const latestEvent = [...events].sort((a, b) => b.created_at - a.created_at)[0];
await db.createMetadata(latestEvent);
return JSON.parse(latestEvent.content) as NDKUserProfile;
} else {
const profile: NDKUserProfile = JSON.parse(embed); const profile: NDKUserProfile = JSON.parse(embed);
return profile; return profile;
} }
const cleanPubkey = pubkey.replace('-', '');
const user = ndk.getUser({ hexpubkey: cleanPubkey });
const profile = await user.fetchProfile({ closeOnEose: true });
if (!user.profile) return Promise.reject(new Error('profile not found'));
await db.createProfile(cleanPubkey, profile);
return user.profile;
}, },
{ {
enabled: !!ndk, enabled: !!ndk,