diff --git a/src/components/Internal/Reactions.tsx b/src/components/Internal/Reactions.tsx index 4e69f13..d990429 100644 --- a/src/components/Internal/Reactions.tsx +++ b/src/components/Internal/Reactions.tsx @@ -1,3 +1,4 @@ +import { Dots } from 'components/Spinner' import { useReactions } from 'hooks' import { Addressable } from 'types' @@ -19,15 +20,13 @@ export const Reactions = ({ addressable }: ReactionsProps) => { aTag: addressable.aTag }) - if (!isDataLoaded) return null - return ( <>
handleReaction(true)} + onClick={isDataLoaded ? () => handleReaction(true) : undefined} >
{
-

{likesCount}

+

+ {isDataLoaded ? likesCount : } +

@@ -50,7 +51,7 @@ export const Reactions = ({ addressable }: ReactionsProps) => { className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactDown ${ hasReactedNegatively ? 'IBMSMSMBSS_D_CRDActive' : '' }`} - onClick={() => handleReaction()} + onClick={isDataLoaded ? () => handleReaction() : undefined} >
{
-

{disLikesCount}

+

+ {isDataLoaded ? disLikesCount : } +

diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..786c6ba --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,9 @@ +import styles from '../styles/dotsSpinner.module.scss' + +export const Spinner = () => ( +
+
+
+) + +export const Dots = () => diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index c81dd7a..b9a87b3 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -1,4 +1,5 @@ import { NDKEvent } from '@nostr-dev-kit/ndk' +import { Dots, Spinner } from 'components/Spinner' import { ZapPopUp } from 'components/Zap' import { formatDate } from 'date-fns' import { @@ -59,6 +60,15 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { author: AuthorFilterEnum.All_Comments }) + const [isLoading, setIsLoading] = useState(true) + useEffect(() => { + // Initial loading to indicate comments fetching (stop after 5 seconds) + const t = window.setTimeout(() => setIsLoading(false), 5000) + return () => { + window.clearTimeout(t) + } + }, []) + useEffect(() => { setCommentCount(commentEvents.length) }, [commentEvents, setCommentCount]) @@ -175,8 +185,18 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { return true } + const handleDiscoveredClick = () => { + setVisible(commentEvents) + } + const [visible, setVisible] = useState([]) + useEffect(() => { + if (isLoading) { + setVisible(commentEvents) + } + }, [commentEvents, isLoading]) + const comments = useMemo(() => { - let filteredComments = commentEvents + let filteredComments = visible if (filterOptions.author === AuthorFilterEnum.Creator_Comments) { filteredComments = filteredComments.filter( (comment) => comment.pubkey === addressable.author @@ -190,14 +210,28 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { } return filteredComments - }, [commentEvents, filterOptions, addressable.author]) + }, [visible, filterOptions.author, filterOptions.sort, addressable.author]) + const discoveredCount = commentEvents.length - visible.length return (

Comments

{/* Hide comment form if aTag is missing */} {!!addressable.aTag && } +
+ {isLoading ? ( + + ) : ( + + )} +
{
- + {profile?.displayName || profile?.name || ''}{' '} - - + + {hexToNpub(props.pubkey)} - +
@@ -447,15 +481,13 @@ const Reactions = (props: Event) => { eTag: props.id }) - if (!isDataLoaded) return null - return ( <>
handleReaction(true)} + onClick={isDataLoaded ? () => handleReaction(true) : undefined} > { > -

{likesCount}

+

+ {isDataLoaded ? likesCount : } +

@@ -476,7 +510,7 @@ const Reactions = (props: Event) => { className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${ hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : '' }`} - onClick={() => handleReaction()} + onClick={isDataLoaded ? () => handleReaction() : undefined} > { > -

{disLikesCount}

+

+ {isDataLoaded ? disLikesCount : } +

diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index 23f672c..ed80c1f 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -21,7 +21,8 @@ import { log, LogType, npubToHex, - orderEventsChronologically + orderEventsChronologically, + timeout } from 'utils' type FetchModsOptions = { @@ -241,8 +242,11 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { hexKey: string, userRelaysType: UserRelaysType ): Promise => { - // Find the user's relays. - const relayUrls = await getRelayListForUser(hexKey, ndk) + // Find the user's relays (10s timeout). + const relayUrls = await Promise.race([ + getRelayListForUser(hexKey, ndk), + timeout(10000) + ]) .then((ndkRelayList) => { if (ndkRelayList) return ndkRelayList[userRelaysType] return [] // Return an empty array if ndkRelayList is undefined diff --git a/src/hooks/useReactions.ts b/src/hooks/useReactions.ts index 574c3eb..0f029bb 100644 --- a/src/hooks/useReactions.ts +++ b/src/hooks/useReactions.ts @@ -5,7 +5,7 @@ import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useMemo, useState } from 'react' import { toast } from 'react-toastify' import { UserRelaysType } from 'types' -import { abbreviateNumber, log, LogType, now } from 'utils' +import { abbreviateNumber, log, LogType, now, timeout } from 'utils' type UseReactionsParams = { pubkey: string @@ -32,7 +32,11 @@ export const useReactions = (params: UseReactionsParams) => { filter['#e'] = [params.eTag] } - fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read) + // 1 minute timeout + Promise.race([ + fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read), + timeout(60000) + ]) .then((events) => { setReactionEvents(events) }) diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 19cbb90..c7aef26 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -36,6 +36,7 @@ import 'swiper/css' import 'swiper/css/navigation' import 'swiper/css/pagination' import { LoadingSpinner } from 'components/LoadingSpinner' +import { Spinner } from 'components/Spinner' export const HomePage = () => { const navigate = useNavigate() @@ -310,14 +311,6 @@ const DisplayLatestMods = () => { ) } -const Spinner = () => { - return ( -
-
-
- ) -} - const DisplayLatestBlogs = () => { const [blogs, setBlogs] = useState[]>() const { fetchEvents } = useNDKContext() diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index ca942cf..56758a6 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -14,17 +14,11 @@ import { useNDKContext, useNSFWList } from 'hooks' -import { kinds, nip19, UnsignedEvent } from 'nostr-tools' +import { kinds, UnsignedEvent } from 'nostr-tools' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { - useParams, - Navigate, - Link, - useLoaderData, - useNavigation -} from 'react-router-dom' +import { Link, useLoaderData, useNavigation } from 'react-router-dom' import { toast } from 'react-toastify' -import { appRoutes, getProfilePageRoute } from 'routes' +import { appRoutes } from 'routes' import { BlogCardDetails, FilterOptions, @@ -38,8 +32,6 @@ import { copyTextToClipboard, DEFAULT_FILTER_OPTIONS, extractBlogCardDetails, - log, - LogType, now, npubToHex, scrollIntoView, @@ -52,23 +44,11 @@ import { BlogCard } from 'components/BlogCard' export const ProfilePage = () => { const { + profilePubkey, profile, isBlocked: _isBlocked, isOwnProfile } = useLoaderData() as ProfilePageLoaderResult - // Try to decode nprofile parameter - const { nprofile } = useParams() - let profilePubkey: string | undefined - try { - const value = nprofile - ? nip19.decode(nprofile as `nprofile1${string}`) - : undefined - profilePubkey = value?.data.pubkey - } catch (error) { - // Silently ignore and redirect to home or logged in user - log(true, LogType.Error, 'Failed to decode nprofile.', error) - } - const scrollTargetRef = useRef(null) const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext() const userState = useAppSelector((state) => state.user) @@ -292,22 +272,6 @@ export const ProfilePage = () => { profilePubkey ) - // Redirect route - let profileRoute = appRoutes.home - if (!nprofile && userState.auth && userState.user) { - // Redirect to user's profile is no profile is linked - const userHexKey = npubToHex(userState.user.npub as string) - - if (userHexKey) { - profileRoute = getProfilePageRoute( - nip19.nprofileEncode({ - pubkey: userHexKey - }) - ) - } - } - if (!profilePubkey) return - return (
diff --git a/src/pages/profile/loader.ts b/src/pages/profile/loader.ts index aecb15d..f3ac4c0 100644 --- a/src/pages/profile/loader.ts +++ b/src/pages/profile/loader.ts @@ -4,9 +4,10 @@ import { LoaderFunctionArgs, redirect } from 'react-router-dom' import { appRoutes, getProfilePageRoute } from 'routes' import { store } from 'store' import { MuteLists, UserProfile } from 'types' -import { log, LogType } from 'utils' +import { log, LogType, npubToHex } from 'utils' export interface ProfilePageLoaderResult { + profilePubkey: string profile: UserProfile isBlocked: boolean isOwnProfile: boolean @@ -24,10 +25,25 @@ export const profileRouteLoader = const { nprofile } = params let profilePubkey: string | undefined try { - const value = nprofile - ? nip19.decode(nprofile as `nprofile1${string}`) - : undefined - profilePubkey = value?.data.pubkey + // Decode if it starts with nprofile1 + if (nprofile?.startsWith('nprofile1')) { + const value = nprofile + ? nip19.decode(nprofile as `nprofile1${string}`) + : undefined + profilePubkey = value?.data.pubkey + } else if (nprofile?.startsWith('npub1')) { + // Try to get hex from the npub and encode it to nprofile + const value = npubToHex(nprofile) + if (value) { + return redirect( + getProfilePageRoute( + nip19.nprofileEncode({ + pubkey: value + }) + ) + ) + } + } } catch (error) { // Silently ignore and redirect to home or logged in user log(true, LogType.Error, 'Failed to decode nprofile.', error) @@ -57,6 +73,7 @@ export const profileRouteLoader = // Empty result const result: ProfilePageLoaderResult = { + profilePubkey: profilePubkey, profile: {}, isBlocked: false, isOwnProfile: false, diff --git a/src/styles/dotsSpinner.module.scss b/src/styles/dotsSpinner.module.scss new file mode 100644 index 0000000..772dfa8 --- /dev/null +++ b/src/styles/dotsSpinner.module.scss @@ -0,0 +1,18 @@ +.loading::after { + content: '.'; + animation: dots 1.5s steps(4, end) infinite; +} + +@keyframes dots { + 0%, + 20% { + content: '.\00a0\00a0'; + } + 40% { + content: '..\00a0'; + } + 60%, + 100% { + content: '...'; + } +}