From d6bc3b868447ff56ba522de2505e692f00f071b2 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 13 Nov 2024 16:24:19 +0100 Subject: [PATCH 1/8] fix(profile): accept npub as valid profile param Closes #120 --- src/pages/profile/index.tsx | 44 ++++--------------------------------- src/pages/profile/loader.ts | 27 ++++++++++++++++++----- 2 files changed, 26 insertions(+), 45 deletions(-) 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, From 414043804444dba06e2bbd53ae204e7f6722477d Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 13 Nov 2024 16:41:15 +0100 Subject: [PATCH 2/8] fix(comments): link to profile from name and npub Closes #117 --- src/components/comment/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index c81dd7a..efd2dc6 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -356,12 +356,12 @@ const Comment = (props: CommentEvent) => {
From 7b1a70446d3df7b2dcda875f4506a40159912d28 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 14 Nov 2024 11:29:01 +0100 Subject: [PATCH 3/8] feat: spinner and new dots loader --- src/components/Spinner.tsx | 9 +++++++++ src/pages/home.tsx | 9 +-------- src/styles/dotsSpinner.module.scss | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/components/Spinner.tsx create mode 100644 src/styles/dotsSpinner.module.scss diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 0000000..8398991 --- /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/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/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: '...'; + } +} From aaffc564243e3226e014cc385aaf413309ac3fbc Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 14 Nov 2024 11:31:13 +0100 Subject: [PATCH 4/8] refactor(reactions): use dots loader and block interaction while loading --- src/components/Internal/Reactions.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 : } +

From cd3c7ace01a2e8e0b0284ca34f51ca4666bb935e Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 14 Nov 2024 13:55:48 +0100 Subject: [PATCH 5/8] refactor(comments): add dots to comment reactions --- src/components/comment/index.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index efd2dc6..eef9997 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 } from 'components/Spinner' import { ZapPopUp } from 'components/Zap' import { formatDate } from 'date-fns' import { @@ -447,15 +448,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 +477,7 @@ const Reactions = (props: Event) => { className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${ hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : '' }`} - onClick={() => handleReaction()} + onClick={isDataLoaded ? () => handleReaction() : undefined} > { > -

{disLikesCount}

+

+ {isDataLoaded ? disLikesCount : } +

From f7d21807a4552d0cfc1161c64dd7854d488ae7c5 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 14 Nov 2024 13:56:42 +0100 Subject: [PATCH 6/8] refactor(fetch): add 1min timeout on reactions, 10sec timeout on user relay list --- src/contexts/NDKContext.tsx | 10 +++++++--- src/hooks/useReactions.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) 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) }) From 18bbc127768f81b46bf61a407f71c85cb3557edb Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 14 Nov 2024 14:38:52 +0100 Subject: [PATCH 7/8] fix(comments): add initial loading indicator 15sec --- src/components/Spinner.tsx | 2 +- src/components/comment/index.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index 8398991..786c6ba 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -6,4 +6,4 @@ export const Spinner = () => (
) -export const Dots = () =>
+export const Dots = () => diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index eef9997..6e1a030 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -1,5 +1,5 @@ import { NDKEvent } from '@nostr-dev-kit/ndk' -import { Dots } from 'components/Spinner' +import { Dots, Spinner } from 'components/Spinner' import { ZapPopUp } from 'components/Zap' import { formatDate } from 'date-fns' import { @@ -60,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 15 seconds) + const t = window.setTimeout(() => setIsLoading(false), 15000) + return () => { + window.clearTimeout(t) + } + }, []) + useEffect(() => { setCommentCount(commentEvents.length) }, [commentEvents, setCommentCount]) @@ -208,6 +217,7 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { ))}
+ {isLoading && }
) From 2ed81c857c999606aad4b3d26d764673725acc1d Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 14 Nov 2024 16:50:37 +0100 Subject: [PATCH 8/8] refactor(comments): reduce initial load wait and add discovered --- src/components/comment/index.tsx | 33 +++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/comment/index.tsx b/src/components/comment/index.tsx index 6e1a030..b9a87b3 100644 --- a/src/components/comment/index.tsx +++ b/src/components/comment/index.tsx @@ -62,8 +62,8 @@ export const Comments = ({ addressable, setCommentCount }: Props) => { const [isLoading, setIsLoading] = useState(true) useEffect(() => { - // Initial loading to indicate comments fetching (stop after 15 seconds) - const t = window.setTimeout(() => setIsLoading(false), 15000) + // Initial loading to indicate comments fetching (stop after 5 seconds) + const t = window.setTimeout(() => setIsLoading(false), 5000) return () => { window.clearTimeout(t) } @@ -185,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 @@ -200,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 ? ( + + ) : ( + + )} +
{ ))}
- {isLoading && }
)