Fixes for #120, #117, #84, #104 #123

Merged
enes merged 8 commits from fixes-120-117-84-104 into staging 2024-11-14 16:02:05 +00:00
9 changed files with 124 additions and 76 deletions

View File

@ -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 (
<>
<div
className={`IBMSMSMBSS_Details_Card IBMSMSMBSS_D_CReactUp ${
hasReactedPositively ? 'IBMSMSMBSS_D_CRUActive' : ''
}`}
onClick={() => handleReaction(true)}
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
@ -41,7 +40,9 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>{likesCount}</p>
<p className='IBMSMSMBSS_Details_CardText'>
{isDataLoaded ? likesCount : <Dots />}
</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
@ -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}
>
<div className='IBMSMSMBSS_Details_CardVisual'>
<svg
@ -64,7 +65,9 @@ export const Reactions = ({ addressable }: ReactionsProps) => {
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
</svg>
</div>
<p className='IBMSMSMBSS_Details_CardText'>{disLikesCount}</p>
<p className='IBMSMSMBSS_Details_CardText'>
{isDataLoaded ? disLikesCount : <Dots />}
</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>

View File

@ -0,0 +1,9 @@
import styles from '../styles/dotsSpinner.module.scss'
export const Spinner = () => (
<div className='spinner'>
<div className='spinnerCircle'></div>
</div>
)
export const Dots = () => <span className={styles.loading}></span>

View File

@ -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<CommentEvent[]>([])
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 (
<div className='IBMSMSMBSSCommentsWrapper'>
<h4 className='IBMSMSMBSSTitle'>Comments</h4>
<div className='IBMSMSMBSSComments'>
{/* Hide comment form if aTag is missing */}
{!!addressable.aTag && <CommentForm handleSubmit={handleSubmit} />}
<div>
{isLoading ? (
<Spinner />
) : (
<button
type='button'
className='btnMain'
onClick={discoveredCount ? handleDiscoveredClick : undefined}
>
<span>Load {discoveredCount} discovered comments</span>
</button>
)}
</div>
<Filter
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
@ -356,12 +390,12 @@ const Comment = (props: CommentEvent) => {
</div>
<div className='IBMSMSMBSSCL_CommentTopDetailsWrapper'>
<div className='IBMSMSMBSSCL_CommentTopDetails'>
<a className='IBMSMSMBSSCL_CTD_Name' href='profile.html'>
<Link className='IBMSMSMBSSCL_CTD_Name' to={profileRoute}>
{profile?.displayName || profile?.name || ''}{' '}
</a>
<a className='IBMSMSMBSSCL_CTD_Address' href='profile.html'>
</Link>
<Link className='IBMSMSMBSSCL_CTD_Address' to={profileRoute}>
{hexToNpub(props.pubkey)}
</a>
</Link>
</div>
<div className='IBMSMSMBSSCL_CommentActionsDetails'>
<a className='IBMSMSMBSSCL_CADTime'>
@ -447,15 +481,13 @@ const Reactions = (props: Event) => {
eTag: props.id
})
if (!isDataLoaded) return null
return (
<>
<div
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEUp ${
hasReactedPositively ? 'IBMSMSMBSSCL_CAEUpActive' : ''
}`}
onClick={() => handleReaction(true)}
onClick={isDataLoaded ? () => handleReaction(true) : undefined}
>
<svg
xmlns='http://www.w3.org/2000/svg'
@ -467,7 +499,9 @@ const Reactions = (props: Event) => {
>
<path d='M0 190.9V185.1C0 115.2 50.52 55.58 119.4 44.1C164.1 36.51 211.4 51.37 244 84.02L256 96L267.1 84.02C300.6 51.37 347 36.51 392.6 44.1C461.5 55.58 512 115.2 512 185.1V190.9C512 232.4 494.8 272.1 464.4 300.4L283.7 469.1C276.2 476.1 266.3 480 256 480C245.7 480 235.8 476.1 228.3 469.1L47.59 300.4C17.23 272.1 .0003 232.4 .0003 190.9L0 190.9z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>{likesCount}</p>
<p className='IBMSMSMBSSCL_CAElementText'>
{isDataLoaded ? likesCount : <Dots />}
</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>
@ -476,7 +510,7 @@ const Reactions = (props: Event) => {
className={`IBMSMSMBSSCL_CAElement IBMSMSMBSSCL_CAEDown ${
hasReactedNegatively ? 'IBMSMSMBSSCL_CAEDownActive' : ''
}`}
onClick={() => handleReaction()}
onClick={isDataLoaded ? () => handleReaction() : undefined}
>
<svg
xmlns='http://www.w3.org/2000/svg'
@ -488,7 +522,9 @@ const Reactions = (props: Event) => {
>
<path d='M512 440.1C512 479.9 479.7 512 439.1 512H71.92C32.17 512 0 479.8 0 440c0-35.88 26.19-65.35 60.56-70.85C43.31 356 32 335.4 32 312C32 272.2 64.25 240 104 240h13.99C104.5 228.2 96 211.2 96 192c0-35.38 28.56-64 63.94-64h16C220.1 128 256 92.12 256 48c0-17.38-5.784-33.35-15.16-46.47C245.8 .7754 250.9 0 256 0c53 0 96 43 96 96c0 11.25-2.288 22-5.913 32h5.879C387.3 128 416 156.6 416 192c0 19.25-8.59 36.25-22.09 48H408C447.8 240 480 272.2 480 312c0 23.38-11.38 44.01-28.63 57.14C485.7 374.6 512 404.3 512 440.1z'></path>
</svg>
<p className='IBMSMSMBSSCL_CAElementText'>{disLikesCount}</p>
<p className='IBMSMSMBSSCL_CAElementText'>
{isDataLoaded ? disLikesCount : <Dots />}
</p>
<div className='IBMSMSMBSSCL_CAElementLoadWrapper'>
<div className='IBMSMSMBSSCL_CAElementLoad'></div>
</div>

View File

@ -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<NDKEvent[]> => {
// 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

View File

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

View File

@ -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 (
<div className='spinner'>
<div className='spinnerCircle'></div>
</div>
)
}
const DisplayLatestBlogs = () => {
const [blogs, setBlogs] = useState<Partial<BlogCardDetails>[]>()
const { fetchEvents } = useNDKContext()

View File

@ -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<HTMLDivElement>(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 <Navigate to={profileRoute} replace={true} />
return (
<div className='InnerBodyMain'>
<div className='ContainerMain'>

View File

@ -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 {
// 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,

View File

@ -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: '...';
}
}