diff --git a/src/components/repo/IssueComments.tsx b/src/components/repo/IssueComments.tsx
index 73c45a3..5257023 100644
--- a/src/components/repo/IssueComments.tsx
+++ b/src/components/repo/IssueComments.tsx
@@ -3,14 +3,12 @@ import { isDid } from '@atcute/lexicons/syntax'
import { IconMessageCircle } from '@tabler/icons-react'
import { Link } from 'react-router-dom'
import { useIssueComments } from '../../hooks/useIssueComments'
-import { useRecordActors } from '../../hooks/useRecordActors'
-import type { ResolvedActor } from '../../lib/actor'
+import { useVisibleActor } from '../../hooks/useVisibleActor'
import type { CommentRecord } from '../../lib/tangled/feed'
import { ProfileAvatar } from '../profile/ProfileAvatar'
import { MarkdownContent } from '../shared/MarkdownContent'
import { SurfaceCard } from '../shared/SurfaceCard'
-const EMPTY_COMMENTS: readonly CommentRecord[] = []
const COMMENT_SKELETON_COUNT = 3
type IssueCommentsProps = {
@@ -19,7 +17,6 @@ type IssueCommentsProps = {
export function IssueComments({ issueUri }: IssueCommentsProps) {
const { comments, error } = useIssueComments(issueUri)
- const { actors, error: actorsError } = useRecordActors(comments?.items ?? EMPTY_COMMENTS)
if (error) {
return
Could not load comments: {error.message}
@@ -39,20 +36,12 @@ export function IssueComments({ issueUri }: IssueCommentsProps) {
Comments ({comments.items.length})
- {actorsError && (
- Some commenter profiles could not be resolved.
- )}
- {comments.items.map((comment) => {
- const authorIdentifier = parseResourceUri(comment.uri).repo
- const author = isDid(authorIdentifier) ? actors?.get(authorIdentifier) : undefined
-
- return (
- -
-
-
- )
- })}
+ {comments.items.map((comment) => (
+ -
+
+
+ ))}
)
@@ -92,41 +81,47 @@ function IssueCommentsSkeleton() {
}
type CommentViewProps = {
- author?: ResolvedActor
comment: CommentRecord
}
-function CommentView({ author, comment }: CommentViewProps) {
- const authorDid = parseResourceUri(comment.uri).repo
- const authorLabel = author?.miniDoc.handle ?? authorDid
+function CommentView({ comment }: CommentViewProps) {
+ const authorIdentifier = parseResourceUri(comment.uri).repo
+ const authorDid = isDid(authorIdentifier) ? authorIdentifier : null
+ const { actor, elementRef } = useVisibleActor(authorDid)
+ const authorLabel = actor?.miniDoc.handle ?? authorIdentifier
return (
-
-
- {author === undefined ? (
- {authorLabel}
- ) : (
-
-
- {authorLabel}
-
- )}
-
-
-
- {comment.value.body.text}
-
-
+
+
+
+ {actor === null ? (
+
+
+ {authorLabel}
+
+ ) : (
+
+
+ {authorLabel}
+
+ )}
+
+
+
+ {comment.value.body.text}
+
+
+
)
}
diff --git a/src/hooks/useElementVisibility.ts b/src/hooks/useElementVisibility.ts
new file mode 100644
index 0000000..dbcf225
--- /dev/null
+++ b/src/hooks/useElementVisibility.ts
@@ -0,0 +1,41 @@
+import { useCallback, useEffect, useState } from 'react'
+
+const DEFAULT_ROOT_MARGIN = '200px'
+
+type UseElementVisibilityOptions = {
+ rootMargin?: string
+}
+
+export function useElementVisibility({
+ rootMargin = DEFAULT_ROOT_MARGIN,
+}: UseElementVisibilityOptions = {}) {
+ const [element, setElement] = useState(null)
+ const [isVisible, setIsVisible] = useState(false)
+ const elementRef = useCallback((node: HTMLElement | null) => {
+ setElement(node)
+ }, [])
+
+ useEffect(() => {
+ if (element === null) return
+
+ if (!('IntersectionObserver' in window)) {
+ setIsVisible(true)
+ return
+ }
+
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (!entry.isIntersecting) return
+
+ setIsVisible(true)
+ observer.disconnect()
+ },
+ { rootMargin },
+ )
+ observer.observe(element)
+
+ return () => observer.disconnect()
+ }, [element, rootMargin])
+
+ return { elementRef, isVisible }
+}
diff --git a/src/hooks/useRecordActors.ts b/src/hooks/useRecordActors.ts
deleted file mode 100644
index 91f771d..0000000
--- a/src/hooks/useRecordActors.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { parseResourceUri, type Did, type ResourceUri } from '@atcute/lexicons'
-import { isDid } from '@atcute/lexicons/syntax'
-import { useEffect, useMemo, useState } from 'react'
-import { resolveActor, type ResolvedActor } from '../lib/actor'
-
-const MAX_CONCURRENT_ACTOR_REQUESTS = 3
-
-type RecordWithUri = {
- uri: ResourceUri
-}
-
-type ActorsState = {
- actors: Map
- error: Error | null
- key: string
-}
-
-export function useRecordActors(records: readonly RecordWithUri[]) {
- const actorDids = useMemo(
- () =>
- [...new Set(records.map(({ uri }) => parseResourceUri(uri).repo))].flatMap((identifier) =>
- isDid(identifier) ? [identifier] : [],
- ),
- [records],
- )
- const actorsKey = [...actorDids].sort().join(',')
- const [state, setState] = useState(null)
-
- useEffect(() => {
- let isCancelled = false
-
- void resolveActors(actorDids).then(({ actors, hasFailures }) => {
- if (isCancelled) return
-
- setState({
- actors,
- error: hasFailures ? new Error('Some record authors could not be resolved') : null,
- key: actorsKey,
- })
- })
-
- return () => {
- isCancelled = true
- }
- }, [actorDids, actorsKey])
-
- const isCurrentList = state?.key === actorsKey
- return {
- actors: isCurrentList ? (state.actors ?? null) : null,
- error: isCurrentList ? (state.error ?? null) : null,
- }
-}
-
-async function resolveActors(actorDids: Did[]) {
- const actors = new Map()
- let hasFailures = false
- let nextActorIndex = 0
- const workerCount = Math.min(MAX_CONCURRENT_ACTOR_REQUESTS, actorDids.length)
-
- await Promise.all(
- Array.from({ length: workerCount }, async () => {
- while (nextActorIndex < actorDids.length) {
- const actorDid = actorDids[nextActorIndex]
- nextActorIndex += 1
-
- try {
- actors.set(actorDid, await resolveActor(actorDid))
- } catch {
- hasFailures = true
- }
- }
- }),
- )
-
- return { actors, hasFailures }
-}
diff --git a/src/hooks/useVisibleActor.ts b/src/hooks/useVisibleActor.ts
new file mode 100644
index 0000000..98d9de0
--- /dev/null
+++ b/src/hooks/useVisibleActor.ts
@@ -0,0 +1,38 @@
+import type { Did } from '@atcute/lexicons'
+import { useEffect, useState } from 'react'
+import { resolveActor, type ResolvedActor } from '../lib/actor'
+import { useElementVisibility } from './useElementVisibility'
+
+type ActorState = {
+ actor?: ResolvedActor
+ did: Did
+}
+
+export function useVisibleActor(did: Did | null) {
+ const { elementRef, isVisible } = useElementVisibility()
+ const [state, setState] = useState(null)
+
+ useEffect(() => {
+ if (did === null || !isVisible) return
+
+ let isCancelled = false
+
+ void resolveActor(did)
+ .then((actor) => {
+ if (!isCancelled) setState({ did, actor })
+ })
+ .catch(() => {
+ if (!isCancelled) setState({ did })
+ })
+
+ return () => {
+ isCancelled = true
+ }
+ }, [did, isVisible])
+
+ return {
+ actor: state?.did === did ? (state.actor ?? null) : null,
+ elementRef,
+ isVisible,
+ }
+}