add notification widget

This commit is contained in:
reya 2023-10-25 09:23:20 +07:00
parent 507628bcaa
commit dcacf23625
21 changed files with 268 additions and 467 deletions

View File

@ -87,7 +87,7 @@
"tauri-controls": "^0.2.0",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.2",
"virtua": "^0.14.0",
"virtua": "^0.15.0",
"zustand": "^4.4.3"
},
"devDependencies": {

View File

@ -213,8 +213,8 @@ dependencies:
specifier: ^0.8.2
version: 0.8.2(@tiptap/core@2.1.12)
virtua:
specifier: ^0.14.0
version: 0.14.0(react-dom@18.2.0)(react@18.2.0)
specifier: ^0.15.0
version: 0.15.0(react-dom@18.2.0)(react@18.2.0)
zustand:
specifier: ^4.4.3
version: 4.4.3(@types/react@18.2.29)(react@18.2.0)
@ -6871,8 +6871,8 @@ packages:
vfile-message: 3.1.4
dev: false
/virtua@0.14.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-+g3fxgFuQCqw6PpU5qzTRKhbSUGOeMEap0VbPaIRB1RiK5MfLiGXIMwID1iX1DmvUC/SqsBsJfVvlUaPNGWSVQ==}
/virtua@0.15.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-kzwin55Tj85tcpNO7p5p7U12+wT6+CJaDSr98BTNKD6t7QEmigDwE7h6dcP170LrY8tW+scsMUtipcroSeSpAw==}
peerDependencies:
react: '>=16.14.0'
react-dom: '>=16.14.0'

View File

@ -65,13 +65,6 @@ export default function App() {
return { Component: UserScreen };
},
},
{
path: 'notifications',
async lazy() {
const { NotificationScreen } = await import('@app/notifications');
return { Component: NotificationScreen };
},
},
{
path: 'nwc',
async lazy() {

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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&apos;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>
);
});

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -17,6 +17,7 @@ import {
LocalFilesWidget,
LocalFollowsWidget,
LocalNetworkWidget,
LocalNotificationWidget,
LocalThreadWidget,
LocalUserWidget,
TrendingAccountsWidget,
@ -74,6 +75,8 @@ export function SpaceScreen() {
return <WidgetList key={widget.id} params={widget} />;
case WidgetKinds.other.learnNostr:
return <LearnNostrWidget key={widget.id} params={widget} />;
case WidgetKinds.local.notification:
return <LocalNotificationWidget key={widget.id} params={widget} />;
default:
return null;
}
@ -83,7 +86,7 @@ export function SpaceScreen() {
useEffect(() => {
fetchWidgets(db);
}, [fetchWidgets]);
}, []);
return (
<VList

View File

@ -4,6 +4,7 @@ import { minidenticon } from 'minidenticons';
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useNDK } from '@libs/ndk/provider';
import { useStorage } from '@libs/storage/provider';
import { AccountMoreActions } from '@shared/accounts/more';
@ -17,6 +18,7 @@ import { sendNativeNotification } from '@utils/notification';
export function ActiveAccount() {
const { db } = useStorage();
const { ndk } = useNDK();
const { status, user } = useProfile(db.account.pubkey);
const { sub } = useNostr();
@ -27,24 +29,38 @@ export function ActiveAccount() {
useEffect(() => {
const filter: NDKFilter = {
'#p': [db.account.pubkey],
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
since: Math.floor(Date.now() / 1000),
'#p': [db.account.pubkey],
};
sub(
filter,
async (event) => {
addActivity(event);
const user = ndk.getUser({ hexpubkey: event.pubkey });
await user.fetchProfile();
switch (event.kind) {
case NDKKind.Text:
return await sendNativeNotification('Mention');
return await sendNativeNotification(
`${user.profile.displayName || user.profile.name} has replied to your note`
);
case NDKKind.Repost:
return await sendNativeNotification('Repost');
return await sendNativeNotification(
`${user.profile.displayName || user.profile.name} has reposted to your note`
);
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:
return await sendNativeNotification('Zap');
return await sendNativeNotification(
`${user.profile.displayName || user.profile.name} has zapped to your note`
);
default:
break;
}
@ -71,7 +87,7 @@ export function ActiveAccount() {
style={{ contentVisibility: 'auto' }}
className="aspect-square h-auto w-full rounded-md"
/>
<Avatar.Fallback delayMs={300}>
<Avatar.Fallback delayMs={150}>
<img
src={svgURI}
alt={db.account.pubkeypubkey}

View 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>
);
}

View File

@ -26,7 +26,7 @@ export function TitleBar({ id, title }: { id?: string; title?: string }) {
) : null}
</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}
</h3>
)}

View File

@ -28,6 +28,7 @@ export const User = memo(function User({
| 'default'
| 'simple'
| 'mention'
| 'notify'
| 'repost'
| 'chat'
| 'large'
@ -51,7 +52,7 @@ export const User = memo(function User({
);
}
if (variant === 'mention') {
if (variant === 'mention' || variant === 'notify') {
return (
<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" />
@ -86,7 +87,7 @@ export const User = memo(function User({
<img
src={svgURI}
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.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') {
return (
<div className="flex h-full w-full flex-col gap-2.5">

View File

@ -6,6 +6,7 @@ export * from './local/thread';
export * from './local/files';
export * from './local/articles';
export * from './local/follows';
export * from './local/notification';
export * from './global/articles';
export * from './global/files';
export * from './global/hashtag';

View 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>
);
}

View File

@ -3,29 +3,20 @@ import { create } from 'zustand';
interface ActivitiesState {
activities: Array<NDKEvent>;
totalNewActivities: number;
setActivities: (events: NDKEvent[], lastLogin: number) => void;
setActivities: (events: NDKEvent[]) => void;
addActivity: (event: NDKEvent) => void;
clearTotalNewActivities: () => void;
}
export const useActivities = create<ActivitiesState>((set) => ({
activities: null,
totalNewActivities: 0,
setActivities: (events: NDKEvent[], lastLogin: number) => {
const totalLatest = events.filter((ev) => ev.created_at > lastLogin)?.length ?? 0;
setActivities: (events: NDKEvent[]) => {
set(() => ({
activities: events,
totalNewActivities: totalLatest,
}));
},
addActivity: (event: NDKEvent) => {
set((state) => ({
activities: state.activities ? [event, ...state.activities] : [event],
totalNewActivities: state.totalNewActivities++,
}));
},
clearTotalNewActivities: () => {
set(() => ({ totalNewActivities: 0 }));
},
}));

View File

@ -24,6 +24,7 @@ export const WidgetKinds = {
user: 104,
thread: 105,
follows: 106,
notification: 107,
},
global: {
feeds: 1000,
@ -109,6 +110,11 @@ export const DefaultWidgets: Array<WidgetGroup> = [
{
title: 'Other',
data: [
{
kind: WidgetKinds.local.notification,
title: 'Notification',
description: 'Everything happens around you',
},
{
kind: WidgetKinds.other.learnNostr,
title: 'Learn Nostr',
@ -125,9 +131,14 @@ export const useWidgets = create<WidgetState>()(
isFetched: false,
fetchWidgets: async (db: LumeStorage) => {
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({
id: '9999',
title: '',

View File

@ -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;
}

View File

@ -1,6 +1,4 @@
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 { NostrEventExt } from 'nostr-fetch';
import { useMemo } from 'react';
@ -10,7 +8,7 @@ import { useStorage } from '@libs/storage/provider';
import { nHoursAgo } from '@utils/date';
import { getMultipleRandom } from '@utils/transform';
import { NDKEventWithReplies, NostrBuildResponse } from '@utils/types';
import { NDKEventWithReplies } from '@utils/types';
export function useNostr() {
const { db } = useStorage();
@ -53,9 +51,6 @@ export function useNostr() {
list.forEach((item) => {
tags.push(['p', item]);
});
// publish event
publish({ content: '', kind: NDKKind.Contacts, tags: tags });
};
const removeContact = async (pubkey: string) => {
@ -66,30 +61,17 @@ export function useNostr() {
list.forEach((item) => {
tags.push(['p', item]);
});
// publish event
publish({ content: '', kind: NDKKind.Contacts, tags: tags });
};
const fetchActivities = async () => {
const getAllActivities = async (limit?: number) => {
try {
const events = await fetcher.fetchAllEvents(
relayUrls,
{
kinds: [
NDKKind.Text,
NDKKind.Contacts,
NDKKind.Repost,
NDKKind.Reaction,
NDKKind.Zap,
],
'#p': [db.account.pubkey],
},
{ since: nHoursAgo(24) },
{ sort: true }
);
const events = await ndk.fetchEvents({
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap],
'#p': [db.account.pubkey],
limit: limit ?? 100,
});
return events as unknown as NDKEvent[];
return [...events];
} catch (e) {
console.error('Error fetching activities', e);
}
@ -289,28 +271,6 @@ export function useNostr() {
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) => {
// @ts-expect-error, NostrEvent to NDKEvent
const ndkEvent = new NDKEvent(ndk, event);
@ -319,87 +279,6 @@ export function useNostr() {
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 {
sub,
addContact,
@ -409,11 +288,9 @@ export function useNostr() {
getContactsByPubkey,
getEventsByPubkey,
getAllRelaysByUsers,
fetchActivities,
getAllActivities,
fetchNIP04Messages,
fetchAllReplies,
publish,
createZap,
upload,
};
}