diff --git a/0 b/0 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/molecules/TaggedItem/TaggedItem.utils.ts b/src/components/molecules/TaggedItem/TaggedItem.utils.ts index 830f2e8f52..c99973a15c 100644 --- a/src/components/molecules/TaggedItem/TaggedItem.utils.ts +++ b/src/components/molecules/TaggedItem/TaggedItem.utils.ts @@ -18,6 +18,11 @@ export function transformTagWithAvatars(tag: NexusTag): TagWithAvatars { }; } +export function transfromTaggersWithAvatars(taggersIds: Pubky[]): { id: Pubky; avatarUrl: string }[] { + const taggers = taggersIds.map((taggerId) => ({ id: taggerId, avatarUrl: FileController.getAvatarUrl(taggerId) })); + return taggers; +} + /** * Transform an array of NexusTags to TagWithAvatars, adding viewer relationship. * Filters out invalid tags (missing label) and resolves avatar URLs. diff --git a/src/components/molecules/TaggedList/TaggedList.tsx b/src/components/molecules/TaggedList/TaggedList.tsx index fec6f4211a..fde716d6ef 100644 --- a/src/components/molecules/TaggedList/TaggedList.tsx +++ b/src/components/molecules/TaggedList/TaggedList.tsx @@ -6,23 +6,34 @@ import { Container } from '@/atoms/Container/Container'; import { Skeleton } from '@/atoms/Skeleton/Skeleton'; import { useInfiniteScroll } from '@/hooks/useInfiniteScroll/useInfiniteScroll'; import { usePostTaggers } from '@/hooks/usePostTaggers/usePostTaggers'; +import { useProfileContext } from '@/providers/ProfileProvider/ProfileProvider'; import { TaggedItem } from '../TaggedItem/TaggedItem'; +import { TagWithAvatars } from '../TaggedItem/TaggedItem.types'; import type { TaggedListProps } from './TaggedList.types'; export function TaggedList({ tags, + hasMore = false, taggedId, taggedKind, - hasMore = false, isLoadingMore = false, onLoadMore, onTagToggle, }: TaggedListProps) { // Track which tag is currently expanded (only one at a time - accordion behavior) const [expandedTagLabel, setExpandedTagLabel] = useState(null); - + const [tagsState, setTagsState] = useState(tags); const shouldFetchTaggers = taggedKind === TagKind.POST && !!taggedId; - const { taggersByLabel, taggerStates, fetchAllTaggers } = usePostTaggers(shouldFetchTaggers ? taggedId : null); + const { taggersByLabel, taggerStates, fetchAllTaggers, fetchTaggedList } = usePostTaggers( + shouldFetchTaggers ? taggedId : null, + ); + const { pubky } = useProfileContext(); + // Use ref for tags to avoid re-triggering the fetch effect when tags update + const tagsRef = useRef(tags); + useEffect(() => { + tagsRef.current = tags; + setTagsState(tags); + }, [tags]); const { sentinelRef } = useInfiniteScroll({ onLoadMore: onLoadMore || (() => {}), @@ -32,28 +43,39 @@ export function TaggedList({ debounceMs: 300, }); - const handleExpandToggle = (tagLabel: string) => { + useEffect(() => { + if (!expandedTagLabel || !shouldFetchTaggers) return; + const selectedTagRef = tagsRef.current.find((tag) => tag.label === expandedTagLabel); + if (!selectedTagRef) return; + const initialIds = selectedTagRef.taggers.map((tagger) => tagger.id); + + void fetchAllTaggers(expandedTagLabel, initialIds, selectedTagRef.taggers_count); + }, [expandedTagLabel, shouldFetchTaggers, fetchAllTaggers]); + + const handleExpandToggle = async (tagLabel: string) => { // Toggle: if clicking the same tag, collapse it; otherwise expand the new one setExpandedTagLabel((prev) => (prev === tagLabel ? null : tagLabel)); - }; + if (tagLabel === expandedTagLabel) return; + const selectedTag = tags.find((tag) => tag.label === tagLabel); + const selectedTagsState = tagsState.find((tag) => tag.label === tagLabel); + if (!selectedTag || !pubky) return; + // Fetch tagger details only once. + if (selectedTagsState?.taggers_count === selectedTagsState?.taggers.length) return; - // Use ref for tags to avoid re-triggering the fetch effect when tags update - const tagsRef = useRef(tags); - useEffect(() => { - tagsRef.current = tags; - }, [tags]); + const response = await fetchTaggedList(tagLabel, pubky, selectedTag.taggers); + if (!response?.allTaggers) { + return; + } + const updatedTagsState = tagsState.map((tag) => + tag.label === tagLabel ? { ...tag, taggers: [...tag.taggers, ...response.allTaggers] } : tag, + ); - useEffect(() => { - if (!expandedTagLabel || !shouldFetchTaggers) return; - const selectedTag = tagsRef.current.find((tag) => tag.label === expandedTagLabel); - if (!selectedTag) return; - const initialIds = selectedTag.taggers.map((tagger) => tagger.id); - void fetchAllTaggers(expandedTagLabel, initialIds, selectedTag.taggers_count); - }, [expandedTagLabel, shouldFetchTaggers, fetchAllTaggers]); + setTagsState(updatedTagsState); + }; return ( - {tags.map((tag) => { + {tagsState.map((tag) => { const tagLabelKey = tag.label.toLowerCase(); const isExpanded = expandedTagLabel === tag.label; const expandedTaggerIds = taggersByLabel.get(tagLabelKey); diff --git a/src/core/controllers/user/user.ts b/src/core/controllers/user/user.ts index 3382b78048..79d9916b81 100644 --- a/src/core/controllers/user/user.ts +++ b/src/core/controllers/user/user.ts @@ -104,7 +104,7 @@ export class UserController { * @param params - The parameters for fetching taggers * @returns The taggers for the user */ - static async fetchTaggers(params: TUserTaggersParams): Promise { + static async fetchTaggers(params: TUserTaggersParams): Promise { return await UserApplication.fetchTaggers(params); } diff --git a/src/hooks/usePostTaggers/usePostTaggers.ts b/src/hooks/usePostTaggers/usePostTaggers.ts index d6877e7ffa..435487ec1f 100644 --- a/src/hooks/usePostTaggers/usePostTaggers.ts +++ b/src/hooks/usePostTaggers/usePostTaggers.ts @@ -2,8 +2,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { PostController } from '@/controllers/post/post'; +import { UserController } from '@/controllers/user/user'; import { Logger } from '@/libs/logger/logger'; import type { Pubky } from '@/models/models.types'; +import { TaggerWithAvatar } from '@/molecules/TaggedItem/TaggedItem.types'; +import { transfromTaggersWithAvatars } from '@/molecules/TaggedItem/TaggedItem.utils'; import type { NexusTaggers } from '@/services/nexus/nexus.types'; import { TAGGERS_PAGE_SIZE } from './usePostTaggers.constants'; import type { TaggersStateMap, UsePostTaggersResult } from './usePostTaggers.types'; @@ -40,6 +43,38 @@ export function usePostTaggers(postId?: string | null): UsePostTaggersResult { * @param initialIds - Initial tagger IDs already known from the tag response * @param totalCount - Expected total count of taggers (used for pagination control) */ + + const fetchTaggedList = async (label: string, userId: string, taggers: TaggerWithAvatar[]) => { + if (!label || !userId) return; + try { + const response = (await UserController.fetchTaggers({ label, user_id: userId })) as NexusTaggers; + const users = response.users; + if (users) { + const ids = new Set(users); + for (const tagger of taggers) { + if (ids.has(tagger.id)) { + ids.delete(tagger.id); + } + } + const allTaggerIds = Array.from(ids); + const taggersWithAvatars = transfromTaggersWithAvatars(allTaggerIds); + const taggersNames = await UserController.getManyDetails({ userIds: allTaggerIds }); + const taggersDetails = taggersWithAvatars.map((tagger) => { + if (taggersNames.has(tagger.id)) { + const taggerName = taggersNames.get(tagger.id)?.name; + return { ...tagger, name: taggerName }; + } else { + // sometimes userdetails aren't fetched so using a placeholder + return { ...tagger, name: 'Anon.' }; + } + }); + return { allTaggers: taggersDetails }; + } + } catch (error) { + Logger.error('[usePostTaggers] Failed to fetch tagged list', { userId, label, error }); + } + }; + const fetchAllTaggers = useCallback( async (label: string, initialIds: Pubky[], totalCount?: number) => { if (!postId) return; @@ -140,5 +175,6 @@ export function usePostTaggers(postId?: string | null): UsePostTaggersResult { taggersByLabel, taggerStates, fetchAllTaggers, + fetchTaggedList, }; } diff --git a/src/hooks/usePostTaggers/usePostTaggers.types.ts b/src/hooks/usePostTaggers/usePostTaggers.types.ts index 0c7db718bc..95dc621a51 100644 --- a/src/hooks/usePostTaggers/usePostTaggers.types.ts +++ b/src/hooks/usePostTaggers/usePostTaggers.types.ts @@ -1,4 +1,5 @@ import type { Pubky } from '@/models/models.types'; +import { TaggerWithAvatar } from '@/molecules/TaggedItem/TaggedItem.types'; export type TaggersState = { ids: Pubky[]; @@ -14,4 +15,18 @@ export interface UsePostTaggersResult { taggersByLabel: Map; taggerStates: TaggersStateMap; fetchAllTaggers: (label: string, initialIds: Pubky[], totalCount?: number) => Promise; + fetchTaggedList: ( + label: string, + user_id: string, + taggers: TaggerWithAvatar[], + ) => Promise< + | { + allTaggers: { + name: string | undefined; + id: Pubky; + avatarUrl: string; + }[]; + } + | undefined + >; }