add notification widget

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

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