diff --git a/package.json b/package.json index 83b67e6e..affc0670 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab3e1f91..05121e25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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' diff --git a/src/app.tsx b/src/app.tsx index 1ec6544a..62edb50a 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -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() { diff --git a/src/app/notifications/components/content.tsx b/src/app/notifications/components/content.tsx deleted file mode 100644 index 1b97277e..00000000 --- a/src/app/notifications/components/content.tsx +++ /dev/null @@ -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 ( - <> - { - const key = children[0] as string; - if (key.startsWith('pub') && key.length > 50 && key.length < 100) - return ; - if (key.startsWith('tag')) return ; - }, - }} - > - {content?.parsed} - - - ); -} diff --git a/src/app/notifications/components/mention.tsx b/src/app/notifications/components/mention.tsx deleted file mode 100644 index 1e918f54..00000000 --- a/src/app/notifications/components/mention.tsx +++ /dev/null @@ -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 ( - -
-
- -

- has mention you · {createdAt} -

-
- - View - -
- - ); -} diff --git a/src/app/notifications/components/reaction.tsx b/src/app/notifications/components/reaction.tsx deleted file mode 100644 index 1043cff2..00000000 --- a/src/app/notifications/components/reaction.tsx +++ /dev/null @@ -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 ( - -
-
- -

- reacted {event.content} · {createdAt} -

-
- - View - -
- - ); -} diff --git a/src/app/notifications/components/repost.tsx b/src/app/notifications/components/repost.tsx deleted file mode 100644 index c50e7e36..00000000 --- a/src/app/notifications/components/repost.tsx +++ /dev/null @@ -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 ( - -
-
- -

- repost{' '} - {event.pubkey !== db.account.pubkey ? 'a post that mention you' : 'your post'}{' '} - · {createdAt} -

-
- - View - -
- - ); -} diff --git a/src/app/notifications/components/simpleNote.tsx b/src/app/notifications/components/simpleNote.tsx deleted file mode 100644 index 963ef47d..00000000 --- a/src/app/notifications/components/simpleNote.tsx +++ /dev/null @@ -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 ( -
- -
- ); - } - - if (status === 'error') { - return ( -
-

Can't get event from relay

-
- ); - } - - return ( -
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" - > - -
-

- {data.content.length > 200 - ? data.content.substring(0, 200) + '...' - : data.content} -

-
-
- ); -}); diff --git a/src/app/notifications/components/user.tsx b/src/app/notifications/components/user.tsx deleted file mode 100644 index 8d4f70d9..00000000 --- a/src/app/notifications/components/user.tsx +++ /dev/null @@ -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 ( -
-
-
- -
-
- ); - } - - return ( -
- {pubkey} - - {user?.name || user?.display_name || user?.displayName || displayNpub(pubkey, 16)} - -
- ); -} diff --git a/src/app/notifications/index.tsx b/src/app/notifications/index.tsx deleted file mode 100644 index 2a2913f6..00000000 --- a/src/app/notifications/index.tsx +++ /dev/null @@ -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 ; - case 6: - return ; - case 7: - return ; - 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 ( -
-
-
- -
- {!activities ? ( -
-
- -

- Loading -

-
-
- ) : activities.length <= 1 ? ( -
-

🎉

-

- Yo!, no new activities around you in the last 24 hours -

-
- ) : ( - activities.map((event) => renderItem(event)) - )} -
-
-
-
- ); -} diff --git a/src/app/space/index.tsx b/src/app/space/index.tsx index 5f2a5186..b0ef9122 100644 --- a/src/app/space/index.tsx +++ b/src/app/space/index.tsx @@ -17,6 +17,7 @@ import { LocalFilesWidget, LocalFollowsWidget, LocalNetworkWidget, + LocalNotificationWidget, LocalThreadWidget, LocalUserWidget, TrendingAccountsWidget, @@ -74,6 +75,8 @@ export function SpaceScreen() { return ; case WidgetKinds.other.learnNostr: return ; + case WidgetKinds.local.notification: + return ; default: return null; } @@ -83,7 +86,7 @@ export function SpaceScreen() { useEffect(() => { fetchWidgets(db); - }, [fetchWidgets]); + }, []); return ( { 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" /> - + {db.account.pubkeypubkey} { + switch (event.kind) { + case NDKKind.Text: + return ; + case NDKKind.Article: + return ; + case 1063: + return ; + default: + return ; + } + }; + + 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 ( +
+
+ +
+
+ ); + } + + return ( +
+
+
+ +

+ {renderText(kind)} +

+
+ + {createdAt} + +
+
+
+ +
+
+
+ {renderKind(data)} + +
+
+
+
+
+ ); +} diff --git a/src/shared/titleBar.tsx b/src/shared/titleBar.tsx index b15838f3..fe3fbb92 100644 --- a/src/shared/titleBar.tsx +++ b/src/shared/titleBar.tsx @@ -26,7 +26,7 @@ export function TitleBar({ id, title }: { id?: string; title?: string }) { ) : null}
) : ( -

+

{title}

)} diff --git a/src/shared/user.tsx b/src/shared/user.tsx index 98cf7a13..da963d08 100644 --- a/src/shared/user.tsx +++ b/src/shared/user.tsx @@ -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 (
@@ -86,7 +87,7 @@ export const User = memo(function User({ {pubkey} @@ -104,6 +105,36 @@ export const User = memo(function User({ ); } + if (variant === 'notify') { + return ( +
+ + + + {pubkey} + + +
+ {user?.name || + user?.display_name || + user?.displayName || + displayNpub(pubkey, 16)} +
+
+ ); + } + if (variant === 'large') { return (
diff --git a/src/shared/widgets/index.ts b/src/shared/widgets/index.ts index 3dd176af..0402c585 100644 --- a/src/shared/widgets/index.ts +++ b/src/shared/widgets/index.ts @@ -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'; diff --git a/src/shared/widgets/local/notification.tsx b/src/shared/widgets/local/notification.tsx new file mode 100644 index 00000000..842cab39 --- /dev/null +++ b/src/shared/widgets/local/notification.tsx @@ -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 ( + + ); + }, + [activities] + ); + + useEffect(() => { + async function getActivities() { + const events = await getAllActivities(48); + setActivities(events); + } + + getActivities(); + }, []); + + return ( + + +
+ {!activities ? ( +
+
+ +

+ Loading... +

+
+
+ ) : activities.length < 1 ? ( +
+

🎉

+

+ Hmm! Nothing new yet. +

+
+ ) : ( + + {activities.map((event) => renderEvent(event))} + + )} +
+
+ ); +} diff --git a/src/stores/activities.ts b/src/stores/activities.ts index 21a1b0f7..1087dd82 100644 --- a/src/stores/activities.ts +++ b/src/stores/activities.ts @@ -3,29 +3,20 @@ import { create } from 'zustand'; interface ActivitiesState { activities: Array; - totalNewActivities: number; - setActivities: (events: NDKEvent[], lastLogin: number) => void; + setActivities: (events: NDKEvent[]) => void; addActivity: (event: NDKEvent) => void; - clearTotalNewActivities: () => void; } export const useActivities = create((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 })); - }, })); diff --git a/src/stores/widgets.ts b/src/stores/widgets.ts index 68fe3562..1b6f3adb 100644 --- a/src/stores/widgets.ts +++ b/src/stores/widgets.ts @@ -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 = [ { 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()( 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: '', diff --git a/src/utils/createBlobFromFile.ts b/src/utils/createBlobFromFile.ts deleted file mode 100644 index 0825c099..00000000 --- a/src/utils/createBlobFromFile.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { readBinaryFile } from '@tauri-apps/plugin-fs'; - -export async function createBlobFromFile(path: string): Promise { - const file = await readBinaryFile(path); - const blob = new Blob([file]); - const arr = new Uint8Array(await blob.arrayBuffer()); - return arr; -} diff --git a/src/utils/hooks/useNostr.ts b/src/utils/hooks/useNostr.ts index 5af41258..706ba5a6 100644 --- a/src/utils/hooks/useNostr.ts +++ b/src/utils/hooks/useNostr.ts @@ -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 => { - 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, }; }