Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
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
Empty file added 0
Empty file.
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
58 changes: 40 additions & 18 deletions src/components/molecules/TaggedList/TaggedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);

const [tagsState, setTagsState] = useState<TagWithAvatars[]>(tags);
Comment thread
Abhishek7Tech marked this conversation as resolved.
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 || (() => {}),
Expand All @@ -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));
};

// 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 selectedTagsState = tagsState.find((tag) => tag.label === tagLabel);
if (!selectedTag || !pubky) return;
// Fetch tagger details only once.
if (selectedTagsState?.taggers_count === selectedTagsState?.taggers.length) 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]);
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) => {
{tagsState.map((tag) => {
const tagLabelKey = tag.label.toLowerCase();
const isExpanded = expandedTagLabel === tag.label;
const expandedTaggerIds = taggersByLabel.get(tagLabelKey);
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
>;
}