diff --git a/src/app/notifications/components/mention.tsx b/src/app/notifications/components/mention.tsx index d9ed7caf..1f681e36 100644 --- a/src/app/notifications/components/mention.tsx +++ b/src/app/notifications/components/mention.tsx @@ -1,32 +1,25 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { useMemo } from 'react'; +import { Link } from 'react-router-dom'; -import { NotiContent } from '@app/notifications/components/content'; import { NotiUser } from '@app/notifications/components/user'; import { formatCreatedAt } from '@utils/createdAt'; -import { parser } from '@utils/parser'; export function NotiMention({ event }: { event: NDKEvent }) { const createdAt = formatCreatedAt(event.created_at); - const content = useMemo(() => parser(event), [event]); + const rootId = event.tags.find((el) => el[0])?.[1]; return ( -
-
-
-
- -

has reply you post · {createdAt}

-
-
-
-
-
- -
+ +
+
+ +

has mention you · {createdAt}

+ + View +
-
+ ); } diff --git a/src/app/notifications/components/reaction.tsx b/src/app/notifications/components/reaction.tsx index 47bb8a6b..ca757236 100644 --- a/src/app/notifications/components/reaction.tsx +++ b/src/app/notifications/components/reaction.tsx @@ -1,30 +1,27 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { Link } from 'react-router-dom'; -import { SimpleNote } from '@app/notifications/components/simpleNote'; import { NotiUser } from '@app/notifications/components/user'; import { formatCreatedAt } from '@utils/createdAt'; export function NotiReaction({ event }: { event: NDKEvent }) { - const root = event.tags.find((e) => e[0] === 'e')?.[1]; const createdAt = formatCreatedAt(event.created_at); + const rootId = event.tags.find((el) => el[0])?.[1]; return ( -
-
-
-
- -

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

-
-
-
-
-
{root && }
+ +
+
+ +

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

+ + View +
-
+ ); } diff --git a/src/app/notifications/components/repost.tsx b/src/app/notifications/components/repost.tsx index 866bfba8..aed68221 100644 --- a/src/app/notifications/components/repost.tsx +++ b/src/app/notifications/components/repost.tsx @@ -1,6 +1,6 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { Link } from 'react-router-dom'; -import { SimpleNote } from '@app/notifications/components/simpleNote'; import { NotiUser } from '@app/notifications/components/user'; import { useStorage } from '@libs/storage/provider'; @@ -10,29 +10,24 @@ import { formatCreatedAt } from '@utils/createdAt'; export function NotiRepost({ event }: { event: NDKEvent }) { const { db } = useStorage(); - const root = event.tags.find((e) => e[0] === 'e')?.[1]; 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} -

-
-
-
-
-
{root && }
+ +
+
+ +

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

+ + View +
-
+ ); } diff --git a/src/app/notifications/components/user.tsx b/src/app/notifications/components/user.tsx index 4400088a..b115f44a 100644 --- a/src/app/notifications/components/user.tsx +++ b/src/app/notifications/components/user.tsx @@ -18,13 +18,13 @@ export function NotiUser({ pubkey }: { pubkey: string }) { } return ( -
+
{pubkey} - + {user?.name || user?.display_name || displayNpub(pubkey, 16)}
diff --git a/src/app/notifications/index.tsx b/src/app/notifications/index.tsx index c743601d..1221e866 100644 --- a/src/app/notifications/index.tsx +++ b/src/app/notifications/index.tsx @@ -1,16 +1,24 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { useCallback } from 'react'; +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 { LoaderIcon } from '@shared/icons'; import { TitleBar } from '@shared/titleBar'; import { useActivities } from '@stores/activities'; +import { useNostr } from '@utils/hooks/useNostr'; + export function NotificationScreen() { - const activities = useActivities((state) => state.activities); + const { fetchActivities } = useNostr(); + const [activities, setActivities, clearTotalNewActivities] = useActivities((state) => [ + state.activities, + state.setActivities, + state.clearTotalNewActivities, + ]); const renderItem = useCallback( (event: NDKEvent) => { @@ -28,24 +36,40 @@ export function NotificationScreen() { [activities] ); + useEffect(() => { + async function getActivities() { + const events = await fetchActivities(); + setActivities(events); + // clear total new activities + clearTotalNewActivities(); + } + + getActivities(); + }, []); + return (
-
-
- {activities?.length < 1 ? ( -
-

🎉

-

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

+
+ {!activities ? ( +
+
+ +

Loading

- ) : ( - activities.map((event) => renderItem(event)) - )} -
+
+ ) : 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 b3de3e62..927f23d6 100644 --- a/src/app/space/index.tsx +++ b/src/app/space/index.tsx @@ -21,6 +21,7 @@ import { XfeedsWidget, XhashtagWidget, } from '@shared/widgets'; +import { LocalFollowsWidget } from '@shared/widgets/local/follows'; import { WidgetKinds, useWidgets } from '@stores/widgets'; @@ -40,6 +41,8 @@ export function SpaceScreen() { switch (widget.kind) { case WidgetKinds.local.network: return ; + case WidgetKinds.local.follows: + return ; case WidgetKinds.local.feeds: return ; case WidgetKinds.local.files: diff --git a/src/app/splash.tsx b/src/app/splash.tsx index 46be95fe..c1b76ff2 100644 --- a/src/app/splash.tsx +++ b/src/app/splash.tsx @@ -7,17 +7,14 @@ import { useStorage } from '@libs/storage/provider'; import { LoaderIcon } from '@shared/icons'; -import { useActivities } from '@stores/activities'; - import { useNostr } from '@utils/hooks/useNostr'; export function SplashScreen() { const { db } = useStorage(); const { ndk } = useNDK(); - const { fetchUserData, fetchActivities, prefetchEvents } = useNostr(); + const { fetchUserData, prefetchEvents } = useNostr(); const [isLoading, setIsLoading] = useState(true); - const setActivities = useActivities((state) => state.setActivities); const skip = async () => { await invoke('close_splashscreen'); @@ -27,11 +24,8 @@ export function SplashScreen() { try { const user = await fetchUserData(); const data = await prefetchEvents(); - const activities = await fetchActivities(); if (user.status === 'ok' && data.status === 'ok') { - // set activities - setActivities(activities); // update last login = current time await db.updateLastLogin(); // close splash screen and open main app screen diff --git a/src/shared/navigation.tsx b/src/shared/navigation.tsx index 8dd7e886..0f0bfb30 100644 --- a/src/shared/navigation.tsx +++ b/src/shared/navigation.tsx @@ -11,10 +11,15 @@ import { ComposerModal } from '@shared/composer'; import { Frame } from '@shared/frame'; import { BellIcon, NavArrowDownIcon, SpaceIcon } from '@shared/icons'; +import { useActivities } from '@stores/activities'; import { useSidebar } from '@stores/sidebar'; +import { compactNumber } from '@utils/number'; + export function Navigation() { const { db } = useStorage(); + + const [totalNewActivities] = useActivities((state) => [state.totalNewActivities]); const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]); return ( @@ -47,17 +52,26 @@ export function Navigation() { preventScrollReset={true} className={({ isActive }) => twMerge( - 'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2', + 'flex h-10 items-center justify-between rounded-r-lg border-l-2 pl-4 pr-2', isActive ? 'border-fuchsia-500 bg-white/5 text-white' : 'border-transparent text-white/80' ) } > - - - - Notifications +
+ + + + Notifications +
+ {totalNewActivities > 0 ? ( +
+ + {compactNumber.format(totalNewActivities)} + +
+ ) : null}
diff --git a/src/shared/notes/kinds/repost.tsx b/src/shared/notes/kinds/repost.tsx index bdcdada2..5fc3a9cb 100644 --- a/src/shared/notes/kinds/repost.tsx +++ b/src/shared/notes/kinds/repost.tsx @@ -19,7 +19,7 @@ import { useEvent } from '@utils/hooks/useEvent'; export function Repost({ event }: { event: NDKEvent }) { // @ts-expect-error, root_id isn't exist on NDKEvent - const { status, data } = useEvent(event.root_id, event.content); + const { status, data } = useEvent(event.root_id ?? event.id, event.content); const renderKind = useCallback( (repostEvent: NDKEvent) => { diff --git a/src/shared/notes/kinds/text.tsx b/src/shared/notes/kinds/text.tsx index 1774fc63..6064f073 100644 --- a/src/shared/notes/kinds/text.tsx +++ b/src/shared/notes/kinds/text.tsx @@ -42,7 +42,7 @@ export function TextNote({ content }: { content: string }) { const cleanURL = new URL(href); cleanURL.search = ''; return ( - + {cleanURL.hostname + cleanURL.pathname} ); diff --git a/src/shared/widgets/local/follows.tsx b/src/shared/widgets/local/follows.tsx index 8769ad46..d435b8dd 100644 --- a/src/shared/widgets/local/follows.tsx +++ b/src/shared/widgets/local/follows.tsx @@ -17,9 +17,9 @@ import { import { NoteSkeleton } from '@shared/notes/skeleton'; import { TitleBar } from '@shared/titleBar'; -import { DBEvent } from '@utils/types'; +import { DBEvent, Widget } from '@utils/types'; -export function LocalFollowsWidget() { +export function LocalFollowsWidget({ params }: { params: Widget }) { const { db } = useStorage(); const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ @@ -116,7 +116,7 @@ export function LocalFollowsWidget() { return (
- +
{status === 'loading' ? (
diff --git a/src/stores/activities.ts b/src/stores/activities.ts index 92affcd7..800019cc 100644 --- a/src/stores/activities.ts +++ b/src/stores/activities.ts @@ -1,27 +1,27 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; interface ActivitiesState { - activities: null | Array; + activities: Array; + totalNewActivities: number; setActivities: (events: NDKEvent[]) => void; addActivity: (event: NDKEvent) => void; + clearTotalNewActivities: () => void; } -export const useActivities = create()( - persist( - (set) => ({ - activities: null, - setActivities: (events: NDKEvent[]) => { - set(() => ({ activities: events })); - }, - addActivity: (event: NDKEvent) => { - set((state) => ({ activities: [event, ...state.activities] })); - }, - }), - { - name: 'activities', - storage: createJSONStorage(() => localStorage), - } - ) -); +export const useActivities = create((set) => ({ + activities: null, + totalNewActivities: 0, + setActivities: (events: NDKEvent[]) => { + set(() => ({ activities: events })); + }, + addActivity: (event: NDKEvent) => { + set((state) => ({ + activities: state.activities ? [event, ...state.activities] : [event], + totalNewActivities: state.totalNewActivities++, + })); + }, + clearTotalNewActivities: () => { + set(() => ({ totalNewActivities: 0 })); + }, +})); diff --git a/src/utils/hooks/useEvent.ts b/src/utils/hooks/useEvent.ts index 70074be8..6b796574 100644 --- a/src/utils/hooks/useEvent.ts +++ b/src/utils/hooks/useEvent.ts @@ -12,7 +12,7 @@ export function useEvent(id: string, embed?: string) { const { status, data } = useQuery( ['event', id], async () => { - // return embed event (nostr.band api) + // return embed event (nostr.band api) or repost if (embed) { const event: NDKEvent = JSON.parse(embed); return event; diff --git a/src/utils/parser.ts b/src/utils/parser.ts index 3e450c91..a80cac1d 100644 --- a/src/utils/parser.ts +++ b/src/utils/parser.ts @@ -3,17 +3,20 @@ import { EventPointer } from 'nostr-tools/lib/nip19'; import { RichContent } from '@utils/types'; -function isURL(str: string) { - const pattern = new RegExp( - '^(https?:\\/\\/)?' + // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', // fragment locator - 'i' - ); - return !!pattern.test(str); +function isURL(string: string) { + try { + const url = new URL(string); + if (url.protocol.length > 0) { + if (url.protocol === 'https:' || url.protocol === 'http:') { + return true; + } else { + return false; + } + } + return true; + } catch (e) { + return false; + } } export function parser(eventContent: string) {