Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/molecules/TaggedItem/TaggedItem.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 25 additions & 36 deletions src/components/molecules/TaggedList/TaggedList.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
'use client';

import { useEffect, useRef, useState } from 'react';
import { TagKind } from '@/application/tag/tag.types';
import { useState } from 'react';
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,
taggedId,
taggedKind,
hasMore = false,
isLoadingMore = false,
onLoadMore,
onTagToggle,
}: TaggedListProps) {
export function TaggedList({ tags, hasMore = false, isLoadingMore = false, onLoadMore, onTagToggle }: TaggedListProps) {
// Track which tag is currently expanded (only one at a time - accordion behavior)
const [expandedTagLabel, setExpandedTagLabel] = useState<string | null>(null);

const shouldFetchTaggers = taggedKind === TagKind.POST && !!taggedId;
const { taggersByLabel, taggerStates, fetchAllTaggers } = usePostTaggers(shouldFetchTaggers ? taggedId : null);
const [tagsState, setTagsState] = useState<TagWithAvatars[]>(tags);
Comment thread
Abhishek7Tech marked this conversation as resolved.
const { fetchTaggedList } = usePostTaggers(null);
const { pubky } = useProfileContext();
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

const { sentinelRef } = useInfiniteScroll({
onLoadMore: onLoadMore || (() => {}),
Expand All @@ -32,43 +25,39 @@ export function TaggedList({
debounceMs: 300,
});

const handleExpandToggle = (tagLabel: string) => {
const handleExpandToggle = async (tagLabel: string) => {
// Toggle: if clicking the same tag, collapse it; otherwise expand the new one
setExpandedTagLabel((prev) => (prev === tagLabel ? null : tagLabel));
};

// Use ref for tags to avoid re-triggering the fetch effect when tags update
const tagsRef = useRef(tags);
useEffect(() => {
tagsRef.current = tags;
}, [tags]);
if (tagLabel === expandedTagLabel) return;
const selectedTag = tags.find((tag) => tag.label === tagLabel);
const selectedTagsRef = tagsState.find((tag) => tag.label === tagLabel);
if (!selectedTag || !pubky) return;

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]);
// Fetch tagger details only once.
if (selectedTagsRef?.taggers_count === selectedTagsRef?.taggers.length) return;

const response = await fetchTaggedList(tagLabel, pubky, selectedTag.taggers);
if (!response?.allTaggers) {
return;
}
const upadtedTagsState = tagsState.map((tag) =>
tag.label === tagLabel ? { ...tag, taggers: [...tag.taggers, ...response.allTaggers] } : tag,
);
setTagsState(upadtedTagsState);
};

return (
<Container className="gap-2">
{tags.map((tag) => {
const tagLabelKey = tag.label.toLowerCase();
{tagsState.map((tag) => {
const isExpanded = expandedTagLabel === tag.label;
const expandedTaggerIds = taggersByLabel.get(tagLabelKey);
const taggerState = taggerStates.get(tagLabelKey);
const isLoadingTaggers = taggerState?.isLoading ?? false;

return (
<TaggedItem
key={tag.label}
tag={tag}
onTagClick={onTagToggle}
isExpanded={isExpanded}
onExpandToggle={handleExpandToggle}
expandedTaggerIds={expandedTaggerIds}
isLoadingTaggers={isLoadingTaggers}
/>
);
})}
Expand Down
2 changes: 1 addition & 1 deletion src/core/controllers/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<NexusTaggers[]> {
static async fetchTaggers(params: TUserTaggersParams): Promise<NexusTaggers[] | NexusTaggers> {
return await UserApplication.fetchTaggers(params);
}

Expand Down
36 changes: 36 additions & 0 deletions src/hooks/usePostTaggers/usePostTaggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Comment thread
Abhishek7Tech marked this conversation as resolved.
if (users) {
const ids = new Set<Pubky>(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;
Expand Down Expand Up @@ -140,5 +175,6 @@ export function usePostTaggers(postId?: string | null): UsePostTaggersResult {
taggersByLabel,
taggerStates,
fetchAllTaggers,
fetchTaggedList,
};
}
15 changes: 15 additions & 0 deletions src/hooks/usePostTaggers/usePostTaggers.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Pubky } from '@/models/models.types';
import { TaggerWithAvatar } from '@/molecules/TaggedItem/TaggedItem.types';

export type TaggersState = {
ids: Pubky[];
Expand All @@ -14,4 +15,18 @@ export interface UsePostTaggersResult {
taggersByLabel: Map<string, Pubky[]>;
taggerStates: TaggersStateMap;
fetchAllTaggers: (label: string, initialIds: Pubky[], totalCount?: number) => Promise<void>;
fetchTaggedList: (
label: string,
user_id: string,
taggers: TaggerWithAvatar[],
) => Promise<
| {
allTaggers: {
name: string | undefined;
id: Pubky;
avatarUrl: string;
}[];
}
| undefined
>;
}