mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-02 18:00:47 +00:00
add notification widget
This commit is contained in:
parent
507628bcaa
commit
dcacf23625
@ -87,7 +87,7 @@
|
|||||||
"tauri-controls": "^0.2.0",
|
"tauri-controls": "^0.2.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"tiptap-markdown": "^0.8.2",
|
"tiptap-markdown": "^0.8.2",
|
||||||
"virtua": "^0.14.0",
|
"virtua": "^0.15.0",
|
||||||
"zustand": "^4.4.3"
|
"zustand": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -213,8 +213,8 @@ dependencies:
|
|||||||
specifier: ^0.8.2
|
specifier: ^0.8.2
|
||||||
version: 0.8.2(@tiptap/core@2.1.12)
|
version: 0.8.2(@tiptap/core@2.1.12)
|
||||||
virtua:
|
virtua:
|
||||||
specifier: ^0.14.0
|
specifier: ^0.15.0
|
||||||
version: 0.14.0(react-dom@18.2.0)(react@18.2.0)
|
version: 0.15.0(react-dom@18.2.0)(react@18.2.0)
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^4.4.3
|
specifier: ^4.4.3
|
||||||
version: 4.4.3(@types/react@18.2.29)(react@18.2.0)
|
version: 4.4.3(@types/react@18.2.29)(react@18.2.0)
|
||||||
@ -6871,8 +6871,8 @@ packages:
|
|||||||
vfile-message: 3.1.4
|
vfile-message: 3.1.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/virtua@0.14.0(react-dom@18.2.0)(react@18.2.0):
|
/virtua@0.15.0(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-+g3fxgFuQCqw6PpU5qzTRKhbSUGOeMEap0VbPaIRB1RiK5MfLiGXIMwID1iX1DmvUC/SqsBsJfVvlUaPNGWSVQ==}
|
resolution: {integrity: sha512-kzwin55Tj85tcpNO7p5p7U12+wT6+CJaDSr98BTNKD6t7QEmigDwE7h6dcP170LrY8tW+scsMUtipcroSeSpAw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=16.14.0'
|
react: '>=16.14.0'
|
||||||
react-dom: '>=16.14.0'
|
react-dom: '>=16.14.0'
|
||||||
|
@ -65,13 +65,6 @@ export default function App() {
|
|||||||
return { Component: UserScreen };
|
return { Component: UserScreen };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'notifications',
|
|
||||||
async lazy() {
|
|
||||||
const { NotificationScreen } = await import('@app/notifications');
|
|
||||||
return { Component: NotificationScreen };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'nwc',
|
path: 'nwc',
|
||||||
async lazy() {
|
async lazy() {
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import ReactMarkdown from 'react-markdown';
|
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
|
|
||||||
import { Hashtag, MentionUser } from '@shared/notes';
|
|
||||||
|
|
||||||
import { RichContent } from '@utils/types';
|
|
||||||
|
|
||||||
export function NotiContent({ content }: { content: RichContent }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ReactMarkdown
|
|
||||||
className="markdown"
|
|
||||||
remarkPlugins={[remarkGfm]}
|
|
||||||
components={{
|
|
||||||
del: ({ children }) => {
|
|
||||||
const key = children[0] as string;
|
|
||||||
if (key.startsWith('pub') && key.length > 50 && key.length < 100)
|
|
||||||
return <MentionUser pubkey={key.replace('pub-', '')} />;
|
|
||||||
if (key.startsWith('tag')) return <Hashtag tag={key.replace('tag-', '')} />;
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{content?.parsed}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { NotiUser } from '@app/notifications/components/user';
|
|
||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
|
||||||
|
|
||||||
export function NotiMention({ event }: { event: NDKEvent }) {
|
|
||||||
const createdAt = formatCreatedAt(event.created_at);
|
|
||||||
const rootId = event.tags.find((el) => el[0])?.[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`/notes/text/${rootId}`} className="h-min w-full px-3">
|
|
||||||
<div className="group flex items-center justify-between rounded-xl px-3 py-3 hover:bg-white/10">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<NotiUser pubkey={event.pubkey} />
|
|
||||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
has mention you · {createdAt}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="hidden text-sm font-semibold text-blue-500 group-hover:block">
|
|
||||||
View
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { NotiUser } from '@app/notifications/components/user';
|
|
||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
|
||||||
|
|
||||||
export function NotiReaction({ event }: { event: NDKEvent }) {
|
|
||||||
const createdAt = formatCreatedAt(event.created_at);
|
|
||||||
const rootId = event.tags.find((el) => el[0])?.[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`/notes/text/${rootId}`} className="h-min w-full px-3">
|
|
||||||
<div className="group flex items-center justify-between rounded-xl px-3 py-3 hover:bg-white/10">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<NotiUser pubkey={event.pubkey} />
|
|
||||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
reacted {event.content} · {createdAt}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="hidden text-sm font-semibold text-blue-500 group-hover:block">
|
|
||||||
View
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { NotiUser } from '@app/notifications/components/user';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { formatCreatedAt } from '@utils/createdAt';
|
|
||||||
|
|
||||||
export function NotiRepost({ event }: { event: NDKEvent }) {
|
|
||||||
const { db } = useStorage();
|
|
||||||
|
|
||||||
const createdAt = formatCreatedAt(event.created_at);
|
|
||||||
const rootId = event.tags.find((el) => el[0])?.[1];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link to={`/notes/text/${rootId}`} className="h-min w-full px-3">
|
|
||||||
<div className="group flex items-center justify-between rounded-xl px-3 py-3 hover:bg-white/10">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<NotiUser pubkey={event.pubkey} />
|
|
||||||
<p className="leading-none text-neutral-600 dark:text-neutral-400">
|
|
||||||
repost{' '}
|
|
||||||
{event.pubkey !== db.account.pubkey ? 'a post that mention you' : 'your post'}{' '}
|
|
||||||
· {createdAt}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span className="hidden text-sm font-semibold text-blue-500 group-hover:block">
|
|
||||||
View
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { NoteSkeleton } from '@shared/notes';
|
|
||||||
import { User } from '@shared/user';
|
|
||||||
|
|
||||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
|
||||||
|
|
||||||
import { useEvent } from '@utils/hooks/useEvent';
|
|
||||||
|
|
||||||
export const SimpleNote = memo(function SimpleNote({ id }: { id: string }) {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { status, data } = useEvent(id);
|
|
||||||
|
|
||||||
const setWidget = useWidgets((state) => state.setWidget);
|
|
||||||
|
|
||||||
const openThread = (event, thread: string) => {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection.toString().length === 0) {
|
|
||||||
setWidget(db, { kind: WidgetKinds.local.thread, title: 'Thread', content: thread });
|
|
||||||
} else {
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === 'loading') {
|
|
||||||
return (
|
|
||||||
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
|
|
||||||
<NoteSkeleton />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'error') {
|
|
||||||
return (
|
|
||||||
<div className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl">
|
|
||||||
<p>Can't get event from relay</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={(e) => openThread(e, id)}
|
|
||||||
onKeyDown={(e) => openThread(e, id)}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
className="mb-2 mt-3 cursor-default rounded-lg bg-white/10 px-3 py-3 backdrop-blur-xl"
|
|
||||||
>
|
|
||||||
<User pubkey={data.pubkey} time={data.created_at} variant="mention" />
|
|
||||||
<div className="markdown">
|
|
||||||
<p>
|
|
||||||
{data.content.length > 200
|
|
||||||
? data.content.substring(0, 200) + '...'
|
|
||||||
: data.content}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,32 +0,0 @@
|
|||||||
import { Image } from '@shared/image';
|
|
||||||
|
|
||||||
import { useProfile } from '@utils/hooks/useProfile';
|
|
||||||
import { displayNpub } from '@utils/shortenKey';
|
|
||||||
|
|
||||||
export function NotiUser({ pubkey }: { pubkey: string }) {
|
|
||||||
const { status, user } = useProfile(pubkey);
|
|
||||||
|
|
||||||
if (status === 'loading') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded-md bg-white/10" />
|
|
||||||
<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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex shrink-0 items-center justify-start gap-2">
|
|
||||||
<Image
|
|
||||||
src={user?.picture || user?.image}
|
|
||||||
alt={pubkey}
|
|
||||||
className="h-8 w-8 shrink-0 rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
<span className="max-w-[10rem] truncate font-medium leading-none text-white">
|
|
||||||
{user?.name || user?.display_name || user?.displayName || displayNpub(pubkey, 16)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,85 +0,0 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
|
|
||||||
import { NotiMention } from '@app/notifications/components/mention';
|
|
||||||
import { NotiReaction } from '@app/notifications/components/reaction';
|
|
||||||
import { NotiRepost } from '@app/notifications/components/repost';
|
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
|
||||||
|
|
||||||
import { LoaderIcon } from '@shared/icons';
|
|
||||||
import { TitleBar } from '@shared/titleBar';
|
|
||||||
|
|
||||||
import { useActivities } from '@stores/activities';
|
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
|
||||||
|
|
||||||
export function NotificationScreen() {
|
|
||||||
const { db } = useStorage();
|
|
||||||
const { fetchActivities } = useNostr();
|
|
||||||
|
|
||||||
const [activities, setActivities, clearTotalNewActivities] = useActivities((state) => [
|
|
||||||
state.activities,
|
|
||||||
state.setActivities,
|
|
||||||
state.clearTotalNewActivities,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
|
||||||
(event: NDKEvent) => {
|
|
||||||
switch (event.kind) {
|
|
||||||
case 1:
|
|
||||||
return <NotiMention key={event.id} event={event} />;
|
|
||||||
case 6:
|
|
||||||
return <NotiRepost key={event.id} event={event} />;
|
|
||||||
case 7:
|
|
||||||
return <NotiReaction key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[activities]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function getActivities() {
|
|
||||||
const events = await fetchActivities();
|
|
||||||
setActivities(events, db.account.last_login_at);
|
|
||||||
}
|
|
||||||
|
|
||||||
getActivities();
|
|
||||||
|
|
||||||
// clear total new activities
|
|
||||||
clearTotalNewActivities();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="h-full w-full overflow-y-auto bg-neutral-400 scrollbar-none dark:bg-neutral-600">
|
|
||||||
<div className="grid h-full grid-cols-3">
|
|
||||||
<div className="col-span-2 flex flex-col border-r border-white/5">
|
|
||||||
<TitleBar title="Activities in the last 24 hours" />
|
|
||||||
<div className="flex h-full flex-col">
|
|
||||||
{!activities ? (
|
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center gap-1.5">
|
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
|
||||||
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
|
||||||
Loading
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : activities.length <= 1 ? (
|
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center">
|
|
||||||
<p className="mb-1 text-4xl">🎉</p>
|
|
||||||
<p className="font-medium text-neutral-600 dark:text-neutral-400">
|
|
||||||
Yo!, no new activities around you in the last 24 hours
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
activities.map((event) => renderItem(event))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -17,6 +17,7 @@ import {
|
|||||||
LocalFilesWidget,
|
LocalFilesWidget,
|
||||||
LocalFollowsWidget,
|
LocalFollowsWidget,
|
||||||
LocalNetworkWidget,
|
LocalNetworkWidget,
|
||||||
|
LocalNotificationWidget,
|
||||||
LocalThreadWidget,
|
LocalThreadWidget,
|
||||||
LocalUserWidget,
|
LocalUserWidget,
|
||||||
TrendingAccountsWidget,
|
TrendingAccountsWidget,
|
||||||
@ -74,6 +75,8 @@ export function SpaceScreen() {
|
|||||||
return <WidgetList key={widget.id} params={widget} />;
|
return <WidgetList key={widget.id} params={widget} />;
|
||||||
case WidgetKinds.other.learnNostr:
|
case WidgetKinds.other.learnNostr:
|
||||||
return <LearnNostrWidget key={widget.id} params={widget} />;
|
return <LearnNostrWidget key={widget.id} params={widget} />;
|
||||||
|
case WidgetKinds.local.notification:
|
||||||
|
return <LocalNotificationWidget key={widget.id} params={widget} />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -83,7 +86,7 @@ export function SpaceScreen() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchWidgets(db);
|
fetchWidgets(db);
|
||||||
}, [fetchWidgets]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VList
|
<VList
|
||||||
|
@ -4,6 +4,7 @@ import { minidenticon } from 'minidenticons';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useNDK } from '@libs/ndk/provider';
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { AccountMoreActions } from '@shared/accounts/more';
|
import { AccountMoreActions } from '@shared/accounts/more';
|
||||||
@ -17,6 +18,7 @@ import { sendNativeNotification } from '@utils/notification';
|
|||||||
|
|
||||||
export function ActiveAccount() {
|
export function ActiveAccount() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
|
const { ndk } = useNDK();
|
||||||
const { status, user } = useProfile(db.account.pubkey);
|
const { status, user } = useProfile(db.account.pubkey);
|
||||||
const { sub } = useNostr();
|
const { sub } = useNostr();
|
||||||
|
|
||||||
@ -27,24 +29,38 @@ export function ActiveAccount() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filter: NDKFilter = {
|
const filter: NDKFilter = {
|
||||||
'#p': [db.account.pubkey],
|
|
||||||
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
||||||
since: Math.floor(Date.now() / 1000),
|
since: Math.floor(Date.now() / 1000),
|
||||||
|
'#p': [db.account.pubkey],
|
||||||
};
|
};
|
||||||
|
|
||||||
sub(
|
sub(
|
||||||
filter,
|
filter,
|
||||||
async (event) => {
|
async (event) => {
|
||||||
addActivity(event);
|
addActivity(event);
|
||||||
|
|
||||||
|
const user = ndk.getUser({ hexpubkey: event.pubkey });
|
||||||
|
await user.fetchProfile();
|
||||||
|
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case NDKKind.Text:
|
case NDKKind.Text:
|
||||||
return await sendNativeNotification('Mention');
|
return await sendNativeNotification(
|
||||||
|
`${user.profile.displayName || user.profile.name} has replied to your note`
|
||||||
|
);
|
||||||
case NDKKind.Repost:
|
case NDKKind.Repost:
|
||||||
return await sendNativeNotification('Repost');
|
return await sendNativeNotification(
|
||||||
|
`${user.profile.displayName || user.profile.name} has reposted to your note`
|
||||||
|
);
|
||||||
case NDKKind.Reaction:
|
case NDKKind.Reaction:
|
||||||
return await sendNativeNotification('Reaction');
|
return await sendNativeNotification(
|
||||||
|
`${user.profile.displayName || user.profile.name} has reacted ${
|
||||||
|
event.content
|
||||||
|
} to your note`
|
||||||
|
);
|
||||||
case NDKKind.Zap:
|
case NDKKind.Zap:
|
||||||
return await sendNativeNotification('Zap');
|
return await sendNativeNotification(
|
||||||
|
`${user.profile.displayName || user.profile.name} has zapped to your note`
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -71,7 +87,7 @@ export function ActiveAccount() {
|
|||||||
style={{ contentVisibility: 'auto' }}
|
style={{ contentVisibility: 'auto' }}
|
||||||
className="aspect-square h-auto w-full rounded-md"
|
className="aspect-square h-auto w-full rounded-md"
|
||||||
/>
|
/>
|
||||||
<Avatar.Fallback delayMs={300}>
|
<Avatar.Fallback delayMs={150}>
|
||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={svgURI}
|
||||||
alt={db.account.pubkeypubkey}
|
alt={db.account.pubkeypubkey}
|
||||||
|
97
src/shared/notification/notifyNote.tsx
Normal file
97
src/shared/notification/notifyNote.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ArticleNote,
|
||||||
|
FileNote,
|
||||||
|
NoteActions,
|
||||||
|
NoteSkeleton,
|
||||||
|
TextNote,
|
||||||
|
UnknownNote,
|
||||||
|
} from '@shared/notes';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
import { formatCreatedAt } from '@utils/createdAt';
|
||||||
|
import { useEvent } from '@utils/hooks/useEvent';
|
||||||
|
|
||||||
|
export function NotifyNote({
|
||||||
|
id,
|
||||||
|
user,
|
||||||
|
content,
|
||||||
|
kind,
|
||||||
|
time,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
user: string;
|
||||||
|
content: string;
|
||||||
|
kind: NDKKind | number;
|
||||||
|
time: number;
|
||||||
|
}) {
|
||||||
|
const createdAt = formatCreatedAt(time, false);
|
||||||
|
const { status, data } = useEvent(id);
|
||||||
|
|
||||||
|
const renderKind = (event: NDKEvent) => {
|
||||||
|
switch (event.kind) {
|
||||||
|
case NDKKind.Text:
|
||||||
|
return <TextNote content={event.content} />;
|
||||||
|
case NDKKind.Article:
|
||||||
|
return <ArticleNote event={event} />;
|
||||||
|
case 1063:
|
||||||
|
return <FileNote event={event} />;
|
||||||
|
default:
|
||||||
|
return <UnknownNote event={event} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderText = (kind: number) => {
|
||||||
|
switch (kind) {
|
||||||
|
case NDKKind.Text:
|
||||||
|
return 'replied';
|
||||||
|
case NDKKind.Reaction:
|
||||||
|
return `reacted ${content}`;
|
||||||
|
case NDKKind.Repost:
|
||||||
|
return 'reposted';
|
||||||
|
case NDKKind.Zap:
|
||||||
|
return 'zapped';
|
||||||
|
default:
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === 'loading') {
|
||||||
|
return (
|
||||||
|
<div className="h-min w-full px-3 pb-3">
|
||||||
|
<div className="relative overflow-hidden rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
||||||
|
<NoteSkeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-5 flex h-min w-full flex-col gap-2 px-3 pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<User pubkey={user} variant="notify" />
|
||||||
|
<p className="font-medium text-neutral-600 dark:text-neutral-400">
|
||||||
|
{renderText(kind)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="font-medium text-neutral-600 dark:text-neutral-400">
|
||||||
|
{createdAt}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative overflow-hidden rounded-xl bg-neutral-100 px-3 py-4 dark:bg-neutral-900">
|
||||||
|
<div className="relative flex flex-col">
|
||||||
|
<User pubkey={data.pubkey} time={data.created_at} eventId={data.id} />
|
||||||
|
<div className="-mt-4 flex items-start gap-3">
|
||||||
|
<div className="w-10 shrink-0" />
|
||||||
|
<div className="relative z-20 flex-1">
|
||||||
|
{renderKind(data)}
|
||||||
|
<NoteActions id={data.id} pubkey={data.pubkey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -26,7 +26,7 @@ export function TitleBar({ id, title }: { id?: string; title?: string }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<h3 className="text-sm font-medium tracking-wide text-neutral-900 dark:text-neutral-100">
|
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
|
@ -28,6 +28,7 @@ export const User = memo(function User({
|
|||||||
| 'default'
|
| 'default'
|
||||||
| 'simple'
|
| 'simple'
|
||||||
| 'mention'
|
| 'mention'
|
||||||
|
| 'notify'
|
||||||
| 'repost'
|
| 'repost'
|
||||||
| 'chat'
|
| 'chat'
|
||||||
| 'large'
|
| 'large'
|
||||||
@ -51,7 +52,7 @@ export const User = memo(function User({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variant === 'mention') {
|
if (variant === 'mention' || variant === 'notify') {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center gap-3">
|
<div className="relative flex items-center gap-3">
|
||||||
<div className="relative z-10 h-6 w-6 shrink-0 animate-pulse overflow-hidden rounded bg-neutral-300 dark:bg-neutral-700" />
|
<div className="relative z-10 h-6 w-6 shrink-0 animate-pulse overflow-hidden rounded bg-neutral-300 dark:bg-neutral-700" />
|
||||||
@ -86,7 +87,7 @@ export const User = memo(function User({
|
|||||||
<img
|
<img
|
||||||
src={svgURI}
|
src={svgURI}
|
||||||
alt={pubkey}
|
alt={pubkey}
|
||||||
className="h-6 w-6 rounded bg-black dark:bg-white"
|
className="h-6 w-6 rounded-md bg-black dark:bg-white"
|
||||||
/>
|
/>
|
||||||
</Avatar.Fallback>
|
</Avatar.Fallback>
|
||||||
</Avatar.Root>
|
</Avatar.Root>
|
||||||
@ -104,6 +105,36 @@ export const User = memo(function User({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variant === 'notify') {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Avatar.Root className="shrink-0">
|
||||||
|
<Avatar.Image
|
||||||
|
src={user?.picture || user?.image}
|
||||||
|
alt={pubkey}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
style={{ contentVisibility: 'auto' }}
|
||||||
|
className="h-8 w-8 rounded-lg"
|
||||||
|
/>
|
||||||
|
<Avatar.Fallback delayMs={300}>
|
||||||
|
<img
|
||||||
|
src={svgURI}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-8 w-8 rounded-lg bg-black dark:bg-white"
|
||||||
|
/>
|
||||||
|
</Avatar.Fallback>
|
||||||
|
</Avatar.Root>
|
||||||
|
<h5 className="max-w-[10rem] truncate font-semibold text-neutral-900 dark:text-neutral-100">
|
||||||
|
{user?.name ||
|
||||||
|
user?.display_name ||
|
||||||
|
user?.displayName ||
|
||||||
|
displayNpub(pubkey, 16)}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (variant === 'large') {
|
if (variant === 'large') {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col gap-2.5">
|
<div className="flex h-full w-full flex-col gap-2.5">
|
||||||
|
@ -6,6 +6,7 @@ export * from './local/thread';
|
|||||||
export * from './local/files';
|
export * from './local/files';
|
||||||
export * from './local/articles';
|
export * from './local/articles';
|
||||||
export * from './local/follows';
|
export * from './local/follows';
|
||||||
|
export * from './local/notification';
|
||||||
export * from './global/articles';
|
export * from './global/articles';
|
||||||
export * from './global/files';
|
export * from './global/files';
|
||||||
export * from './global/hashtag';
|
export * from './global/hashtag';
|
||||||
|
81
src/shared/widgets/local/notification.tsx
Normal file
81
src/shared/widgets/local/notification.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { VList } from 'virtua';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { LoaderIcon } from '@shared/icons';
|
||||||
|
import { NotifyNote } from '@shared/notification/notifyNote';
|
||||||
|
import { TitleBar } from '@shared/titleBar';
|
||||||
|
import { WidgetWrapper } from '@shared/widgets';
|
||||||
|
|
||||||
|
import { useActivities } from '@stores/activities';
|
||||||
|
|
||||||
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
import { Widget } from '@utils/types';
|
||||||
|
|
||||||
|
export function LocalNotificationWidget({ params }: { params: Widget }) {
|
||||||
|
const { db } = useStorage();
|
||||||
|
const { getAllActivities } = useNostr();
|
||||||
|
|
||||||
|
const [activities, setActivities] = useActivities((state) => [
|
||||||
|
state.activities,
|
||||||
|
state.setActivities,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const renderEvent = useCallback(
|
||||||
|
(event: NDKEvent) => {
|
||||||
|
const rootEventId = event.tags.find((el) => el[0] === 'e')?.[1];
|
||||||
|
if (!rootEventId) return null;
|
||||||
|
if (event.pubkey === db.account.pubkey) return null;
|
||||||
|
return (
|
||||||
|
<NotifyNote
|
||||||
|
id={rootEventId}
|
||||||
|
user={event.pubkey}
|
||||||
|
content={event.content}
|
||||||
|
kind={event.kind}
|
||||||
|
time={event.created_at}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[activities]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getActivities() {
|
||||||
|
const events = await getAllActivities(48);
|
||||||
|
setActivities(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActivities();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WidgetWrapper>
|
||||||
|
<TitleBar id={params.id} title={params.title} />
|
||||||
|
<div className="h-full px-3">
|
||||||
|
{!activities ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-1.5">
|
||||||
|
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
|
||||||
|
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||||
|
Loading...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : activities.length < 1 ? (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<p className="mb-1 text-4xl">🎉</p>
|
||||||
|
<p className="text-center font-medium text-neutral-600 dark:text-neutral-400">
|
||||||
|
Hmm! Nothing new yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<VList className="h-full overflow-y-auto scrollbar-none">
|
||||||
|
{activities.map((event) => renderEvent(event))}
|
||||||
|
</VList>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
||||||
|
);
|
||||||
|
}
|
@ -3,29 +3,20 @@ import { create } from 'zustand';
|
|||||||
|
|
||||||
interface ActivitiesState {
|
interface ActivitiesState {
|
||||||
activities: Array<NDKEvent>;
|
activities: Array<NDKEvent>;
|
||||||
totalNewActivities: number;
|
setActivities: (events: NDKEvent[]) => void;
|
||||||
setActivities: (events: NDKEvent[], lastLogin: number) => void;
|
|
||||||
addActivity: (event: NDKEvent) => void;
|
addActivity: (event: NDKEvent) => void;
|
||||||
clearTotalNewActivities: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useActivities = create<ActivitiesState>((set) => ({
|
export const useActivities = create<ActivitiesState>((set) => ({
|
||||||
activities: null,
|
activities: null,
|
||||||
totalNewActivities: 0,
|
setActivities: (events: NDKEvent[]) => {
|
||||||
setActivities: (events: NDKEvent[], lastLogin: number) => {
|
|
||||||
const totalLatest = events.filter((ev) => ev.created_at > lastLogin)?.length ?? 0;
|
|
||||||
set(() => ({
|
set(() => ({
|
||||||
activities: events,
|
activities: events,
|
||||||
totalNewActivities: totalLatest,
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
addActivity: (event: NDKEvent) => {
|
addActivity: (event: NDKEvent) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
activities: state.activities ? [event, ...state.activities] : [event],
|
activities: state.activities ? [event, ...state.activities] : [event],
|
||||||
totalNewActivities: state.totalNewActivities++,
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
clearTotalNewActivities: () => {
|
|
||||||
set(() => ({ totalNewActivities: 0 }));
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
@ -24,6 +24,7 @@ export const WidgetKinds = {
|
|||||||
user: 104,
|
user: 104,
|
||||||
thread: 105,
|
thread: 105,
|
||||||
follows: 106,
|
follows: 106,
|
||||||
|
notification: 107,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
feeds: 1000,
|
feeds: 1000,
|
||||||
@ -109,6 +110,11 @@ export const DefaultWidgets: Array<WidgetGroup> = [
|
|||||||
{
|
{
|
||||||
title: 'Other',
|
title: 'Other',
|
||||||
data: [
|
data: [
|
||||||
|
{
|
||||||
|
kind: WidgetKinds.local.notification,
|
||||||
|
title: 'Notification',
|
||||||
|
description: 'Everything happens around you',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
kind: WidgetKinds.other.learnNostr,
|
kind: WidgetKinds.other.learnNostr,
|
||||||
title: 'Learn Nostr',
|
title: 'Learn Nostr',
|
||||||
@ -125,9 +131,14 @@ export const useWidgets = create<WidgetState>()(
|
|||||||
isFetched: false,
|
isFetched: false,
|
||||||
fetchWidgets: async (db: LumeStorage) => {
|
fetchWidgets: async (db: LumeStorage) => {
|
||||||
const dbWidgets = await db.getWidgets();
|
const dbWidgets = await db.getWidgets();
|
||||||
console.log('db widgets: ', dbWidgets);
|
|
||||||
|
|
||||||
// default: add network widget
|
dbWidgets.unshift({
|
||||||
|
id: '9998',
|
||||||
|
title: 'Notification',
|
||||||
|
content: '',
|
||||||
|
kind: WidgetKinds.local.notification,
|
||||||
|
});
|
||||||
|
|
||||||
dbWidgets.unshift({
|
dbWidgets.unshift({
|
||||||
id: '9999',
|
id: '9999',
|
||||||
title: '',
|
title: '',
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { readBinaryFile } from '@tauri-apps/plugin-fs';
|
|
||||||
|
|
||||||
export async function createBlobFromFile(path: string): Promise<Uint8Array> {
|
|
||||||
const file = await readBinaryFile(path);
|
|
||||||
const blob = new Blob([file]);
|
|
||||||
const arr = new Uint8Array(await blob.arrayBuffer());
|
|
||||||
return arr;
|
|
||||||
}
|
|
@ -1,6 +1,4 @@
|
|||||||
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
|
import { NDKEvent, NDKFilter, NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||||
import { message, open } from '@tauri-apps/plugin-dialog';
|
|
||||||
import { fetch } from '@tauri-apps/plugin-http';
|
|
||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import { NostrEventExt } from 'nostr-fetch';
|
import { NostrEventExt } from 'nostr-fetch';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@ -10,7 +8,7 @@ import { useStorage } from '@libs/storage/provider';
|
|||||||
|
|
||||||
import { nHoursAgo } from '@utils/date';
|
import { nHoursAgo } from '@utils/date';
|
||||||
import { getMultipleRandom } from '@utils/transform';
|
import { getMultipleRandom } from '@utils/transform';
|
||||||
import { NDKEventWithReplies, NostrBuildResponse } from '@utils/types';
|
import { NDKEventWithReplies } from '@utils/types';
|
||||||
|
|
||||||
export function useNostr() {
|
export function useNostr() {
|
||||||
const { db } = useStorage();
|
const { db } = useStorage();
|
||||||
@ -53,9 +51,6 @@ export function useNostr() {
|
|||||||
list.forEach((item) => {
|
list.forEach((item) => {
|
||||||
tags.push(['p', item]);
|
tags.push(['p', item]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// publish event
|
|
||||||
publish({ content: '', kind: NDKKind.Contacts, tags: tags });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeContact = async (pubkey: string) => {
|
const removeContact = async (pubkey: string) => {
|
||||||
@ -66,30 +61,17 @@ export function useNostr() {
|
|||||||
list.forEach((item) => {
|
list.forEach((item) => {
|
||||||
tags.push(['p', item]);
|
tags.push(['p', item]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// publish event
|
|
||||||
publish({ content: '', kind: NDKKind.Contacts, tags: tags });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchActivities = async () => {
|
const getAllActivities = async (limit?: number) => {
|
||||||
try {
|
try {
|
||||||
const events = await fetcher.fetchAllEvents(
|
const events = await ndk.fetchEvents({
|
||||||
relayUrls,
|
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
|
||||||
{
|
'#p': [db.account.pubkey],
|
||||||
kinds: [
|
limit: limit ?? 100,
|
||||||
NDKKind.Text,
|
});
|
||||||
NDKKind.Contacts,
|
|
||||||
NDKKind.Repost,
|
|
||||||
NDKKind.Reaction,
|
|
||||||
NDKKind.Zap,
|
|
||||||
],
|
|
||||||
'#p': [db.account.pubkey],
|
|
||||||
},
|
|
||||||
{ since: nHoursAgo(24) },
|
|
||||||
{ sort: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
return events as unknown as NDKEvent[];
|
return [...events];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error fetching activities', e);
|
console.error('Error fetching activities', e);
|
||||||
}
|
}
|
||||||
@ -289,28 +271,6 @@ export function useNostr() {
|
|||||||
return relayMap;
|
return relayMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
const publish = async ({
|
|
||||||
content,
|
|
||||||
kind,
|
|
||||||
tags,
|
|
||||||
}: {
|
|
||||||
content: string;
|
|
||||||
kind: NDKKind | number;
|
|
||||||
tags: string[][];
|
|
||||||
}): Promise<NDKEvent> => {
|
|
||||||
const event = new NDKEvent(ndk);
|
|
||||||
event.content = content;
|
|
||||||
event.kind = kind;
|
|
||||||
event.created_at = Math.floor(Date.now() / 1000);
|
|
||||||
event.pubkey = db.account.pubkey;
|
|
||||||
event.tags = tags;
|
|
||||||
|
|
||||||
await event.sign();
|
|
||||||
await event.publish();
|
|
||||||
|
|
||||||
return event;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createZap = async (event: NDKEvent, amount: number, message?: string) => {
|
const createZap = async (event: NDKEvent, amount: number, message?: string) => {
|
||||||
// @ts-expect-error, NostrEvent to NDKEvent
|
// @ts-expect-error, NostrEvent to NDKEvent
|
||||||
const ndkEvent = new NDKEvent(ndk, event);
|
const ndkEvent = new NDKEvent(ndk, event);
|
||||||
@ -319,87 +279,6 @@ export function useNostr() {
|
|||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
const upload = async (file: null | string, nip94?: boolean) => {
|
|
||||||
try {
|
|
||||||
let filepath = file;
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
const selected = await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: 'Media',
|
|
||||||
extensions: [
|
|
||||||
'png',
|
|
||||||
'jpeg',
|
|
||||||
'jpg',
|
|
||||||
'gif',
|
|
||||||
'mp4',
|
|
||||||
'mp3',
|
|
||||||
'webm',
|
|
||||||
'mkv',
|
|
||||||
'avi',
|
|
||||||
'mov',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (Array.isArray(selected)) {
|
|
||||||
// user selected multiple files
|
|
||||||
} else if (selected === null) {
|
|
||||||
return {
|
|
||||||
url: null,
|
|
||||||
error: 'Cancelled',
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
filepath = selected.path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', filepath);
|
|
||||||
|
|
||||||
const res: NostrBuildResponse = await fetch(
|
|
||||||
'https://nostr.build/api/v2/upload/files',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'multipart/form-data' },
|
|
||||||
body: formData,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const data = res.data.data[0];
|
|
||||||
const url = data.url;
|
|
||||||
|
|
||||||
if (nip94) {
|
|
||||||
const tags = [
|
|
||||||
['url', url],
|
|
||||||
['x', data.sha256 ?? ''],
|
|
||||||
['m', data.mime ?? 'application/octet-stream'],
|
|
||||||
['size', data.size.toString() ?? '0'],
|
|
||||||
['dim', `${data.dimensions.width}x${data.dimensions.height}` ?? '0'],
|
|
||||||
['blurhash', data.blurhash ?? ''],
|
|
||||||
];
|
|
||||||
|
|
||||||
await publish({ content: '', kind: 1063, tags: tags });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: url,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: null,
|
|
||||||
error: 'Upload failed',
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
await message(e, { title: 'Lume', type: 'error' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sub,
|
sub,
|
||||||
addContact,
|
addContact,
|
||||||
@ -409,11 +288,9 @@ export function useNostr() {
|
|||||||
getContactsByPubkey,
|
getContactsByPubkey,
|
||||||
getEventsByPubkey,
|
getEventsByPubkey,
|
||||||
getAllRelaysByUsers,
|
getAllRelaysByUsers,
|
||||||
fetchActivities,
|
getAllActivities,
|
||||||
fetchNIP04Messages,
|
fetchNIP04Messages,
|
||||||
fetchAllReplies,
|
fetchAllReplies,
|
||||||
publish,
|
|
||||||
createZap,
|
createZap,
|
||||||
upload,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user