Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
136 changes: 72 additions & 64 deletions src/features/social/components/ReplyToFeedItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SharePopover from './SharePopover';
import PostTipButton from './PostTipButton';
import { useWallet } from '../../../hooks';
import { compactTime, fullTimestamp } from '../../../utils/time';
import { useCompactFeedItemLayout } from './useCompactFeedItemLayout';

interface ReplyToFeedItemProps {
item: PostDto;
Expand Down Expand Up @@ -81,6 +82,7 @@ const ReplyToFeedItem = memo(({
const { chainNames, profileDisplayNames } = useWallet();
const displayName = (profileDisplayNames?.[authorAddress] ?? chainNames?.[authorAddress] ?? '').trim();
const hasDisplayName = Boolean(displayName);
const { containerRef, isCompact } = useCompactFeedItemLayout(item.tx_hash ? 700 : 620);

const parentId = useParentId(item);
const [parent, setParent] = useState<PostDto | null>(null);
Expand Down Expand Up @@ -130,6 +132,66 @@ const ReplyToFeedItem = memo(({
const media = Array.isArray(item.media)
? item.media.filter((m) => (typeof m === 'string' ? !m.startsWith('comment:') : true))
: [];
const unnamedHeader = isCompact ? (
<>
<div className="flex items-center gap-2 min-w-0">
<div className="text-[15px] font-semibold text-white truncate" title={authorAddress}>
{formatAddress(authorAddress, 6, true)}
</div>
<span className="text-white/50 shrink-0">·</span>
{item.tx_hash ? (
<BlockchainInfoPopover
txHash={(item as any).tx_hash}
createdAt={item.created_at as unknown as string}
sender={(item as any).sender_address}
contract={(item as any).contract_address}
postId={String(item.id)}
triggerContent={(
<span className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
</span>
)}
/>
) : (
<div className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>{compactTime(item.created_at as unknown as string)}</div>
)}
</div>
<div className="flex items-center gap-1 text-[10px] text-white/60 font-mono min-w-0">
<span className="truncate">{formatAddress(authorAddress, 10, false)}</span>
<InlineCopyButton value={authorAddress} className="shrink-0" />
</div>
</>
) : (
<>
<div className="text-[15px] font-semibold text-white truncate" title={authorAddress}>
{authorAddress}
</div>
<div>
{item.tx_hash ? (
<BlockchainInfoPopover
txHash={(item as any).tx_hash}
createdAt={item.created_at as unknown as string}
sender={(item as any).sender_address}
contract={(item as any).contract_address}
postId={String(item.id)}
triggerContent={(
<span className="text-[10px] text-white/60 truncate" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
{' '}
ago
</span>
)}
/>
) : (
<div className="text-[10px] text-white/60 truncate" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
{' '}
ago
</div>
)}
</div>
</>
);

// Compute total descendant comments (all levels) for this item
const { data: descendantCount } = useQuery<number>({
Expand Down Expand Up @@ -180,6 +242,7 @@ const ReplyToFeedItem = memo(({

return (
<article
ref={containerRef}
className={cn(
'relative w-full px-3 md:px-4 py-4 md:py-5 border-b border-white/10 bg-transparent transition-colors',
!isActive && 'cursor-pointer hover:bg-white/[0.04]',
Expand All @@ -206,29 +269,28 @@ const ReplyToFeedItem = memo(({
sender={item.sender_address}
contract={(item as any).contract_address}
postId={String(item.id)}
className="px-2"
showLabel
className={cn('px-2', isCompact && 'px-0')}
showLabel={!isCompact}
/>
</div>
)}
{/* Main row: avatar left, content right */}
<div className="flex gap-3 items-start">
<div className="flex-shrink-0 pt-0.5">
<div className="md:hidden">
{isCompact ? (
<AddressAvatarWithChainName address={authorAddress} size={36} showAddressAndChainName={false} variant="feed" />
</div>
<div className="hidden md:block">
) : (
<AddressAvatarWithChainName address={authorAddress} size={40} showAddressAndChainName={false} variant="feed" />
</div>
)}
</div>

<div className="flex-1 min-w-0">
<div className={cn('flex-1 min-w-0', item.tx_hash && (isCompact ? 'pr-9' : 'pr-24'))}>
{/* Header: keep named-user layout; show address-first layout for unnamed users */}
<div className="min-w-0">
{hasDisplayName ? (
<>
<div className="flex items-center gap-2">
<div className="text-[15px] font-semibold text-white truncate">
<div className={cn('flex items-center min-w-0', isCompact ? 'gap-1.5' : 'gap-2')}>
<div className={cn('font-semibold text-white truncate min-w-0', isCompact ? 'text-[14px]' : 'text-[15px]')}>
{displayName}
</div>
<span className="text-white/50 shrink-0">·</span>
Expand All @@ -255,61 +317,7 @@ const ReplyToFeedItem = memo(({
</div>
</>
) : (
<>
<div className="flex items-center gap-2 md:hidden">
<div className="text-[15px] font-semibold text-white truncate" title={authorAddress}>
{formatAddress(authorAddress, 6, true)}
</div>
<span className="text-white/50 shrink-0">·</span>
{item.tx_hash ? (
<BlockchainInfoPopover
txHash={(item as any).tx_hash}
createdAt={item.created_at as unknown as string}
sender={(item as any).sender_address}
contract={(item as any).contract_address}
postId={String(item.id)}
triggerContent={(
<span className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
</span>
)}
/>
) : (
<div className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>{compactTime(item.created_at as unknown as string)}</div>
)}
</div>
<div className="md:hidden flex items-center gap-1 text-[10px] text-white/60 font-mono min-w-0">
<span className="truncate">{formatAddress(authorAddress, 10, false)}</span>
<InlineCopyButton value={authorAddress} className="shrink-0" />
</div>
<div className="hidden md:block text-[15px] font-semibold text-white truncate" title={authorAddress}>
{authorAddress}
</div>
<div className="hidden md:block">
{item.tx_hash ? (
<BlockchainInfoPopover
txHash={(item as any).tx_hash}
createdAt={item.created_at as unknown as string}
sender={(item as any).sender_address}
contract={(item as any).contract_address}
postId={String(item.id)}
triggerContent={(
<span className="text-[10px] text-white/60 truncate" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
{' '}
ago
</span>
)}
/>
) : (
<div className="text-[10px] text-white/60 truncate" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
{' '}
ago
</div>
)}
</div>
</>
unnamedHeader
)}
</div>

Expand Down
64 changes: 36 additions & 28 deletions src/features/social/components/TokenCreatedFeedItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SharePopover from './SharePopover';
import { useWallet } from '../../../hooks';
import type { PostDto } from '../../../api/generated';
import { compactTime, fullTimestamp } from '../../../utils/time';
import { useCompactFeedItemLayout } from './useCompactFeedItemLayout';

interface TokenCreatedFeedItemProps {
item: PostDto;
Expand Down Expand Up @@ -42,13 +43,39 @@ const TokenCreatedFeedItem = memo(({ item, onOpenPost }: TokenCreatedFeedItemPro
const authorLabel = displayName || formatAddress(authorAddress, 6, true);
const tokenName = useTokenName(item);
const tokenLink = tokenName ? `/trends/tokens/${tokenName}` : undefined;
const { containerRef, isCompact } = useCompactFeedItemLayout(item.tx_hash ? 700 : 620);
const unnamedHeader = isCompact ? (
<>
<div className="flex items-center gap-2.5 min-w-0">
<div className="text-[15px] font-semibold text-white truncate" title={authorAddress}>
{formatAddress(authorAddress, 6, true)}
</div>
<span className="text-white/50 shrink-0">·</span>
<div className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>{compactTime(item.created_at as unknown as string)}</div>
</div>
<div className="mt-1 flex items-center gap-1 text-[9px] text-white/65 font-mono leading-[1.2] min-w-0">
<span className="truncate">{authorAddress}</span>
<InlineCopyButton value={authorAddress} className="shrink-0" />
</div>
</>
) : (
<>
<div className="text-[15px] font-semibold text-white truncate" title={authorAddress}>
{authorAddress}
</div>
<div className="mt-1 text-[10px] text-white/65 leading-[1.2] truncate" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
</div>
</>
);

const handleOpen = useCallback(() => onOpenPost(postId), [onOpenPost, postId]);
const navigate = useNavigate();

return (
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<article
ref={containerRef}
className={cn(
'relative w-full px-3 md:px-4 py-4 md:py-5 border-b border-white/10 bg-transparent transition-colors hover:bg-white/[0.04]',
)}
Expand All @@ -71,29 +98,28 @@ const TokenCreatedFeedItem = memo(({ item, onOpenPost }: TokenCreatedFeedItemPro
sender={item.sender_address}
contract={(item as any).contract_address}
postId={String(item.id)}
className="px-2"
showLabel
className={cn('px-2', isCompact && 'px-0')}
showLabel={!isCompact}
/>
</div>
)}

<div className="flex gap-2 md:gap-3 items-start">
<div className="flex-shrink-0 pt-0.5">
<div className="md:hidden">
{isCompact ? (
<AddressAvatarWithChainName address={authorAddress} size={34} showAddressAndChainName={false} variant="feed" />
</div>
<div className="hidden md:block">
) : (
<AddressAvatarWithChainName address={authorAddress} size={40} showAddressAndChainName={false} variant="feed" />
</div>
)}
</div>

<div className="flex-1 min-w-0">
<div className={cn('flex-1 min-w-0', item.tx_hash && (isCompact ? 'pr-9' : 'pr-24'))}>
{/* Header: keep named-user layout; show address-first layout for unnamed users */}
{hasDisplayName ? (
<>
<div className="flex items-center justify-between gap-2.5">
<div className="flex items-baseline gap-2.5 min-w-0">
<div className="text-[15px] font-semibold text-white truncate">{displayName}</div>
<div className={cn('flex items-baseline min-w-0', isCompact ? 'gap-2' : 'gap-2.5')}>
<div className={cn('font-semibold text-white truncate min-w-0', isCompact ? 'text-[14px]' : 'text-[15px]')}>{displayName}</div>
<span className="text-white/50 shrink-0">·</span>
<div className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>{compactTime(item.created_at as unknown as string)}</div>
</div>
Expand All @@ -104,25 +130,7 @@ const TokenCreatedFeedItem = memo(({ item, onOpenPost }: TokenCreatedFeedItemPro
</div>
</>
) : (
<>
<div className="flex items-center gap-2.5 min-w-0 md:hidden">
<div className="text-[15px] font-semibold text-white truncate" title={authorAddress}>
{formatAddress(authorAddress, 6, true)}
</div>
<span className="text-white/50 shrink-0">·</span>
<div className="text-[12px] text-white/70 whitespace-nowrap shrink-0" title={fullTimestamp(item.created_at as unknown as string)}>{compactTime(item.created_at as unknown as string)}</div>
</div>
<div className="md:hidden mt-1 flex items-center gap-1 text-[9px] text-white/65 font-mono leading-[1.2] min-w-0">
<span className="truncate">{authorAddress}</span>
<InlineCopyButton value={authorAddress} className="shrink-0" />
</div>
<div className="hidden md:block text-[15px] font-semibold text-white truncate" title={authorAddress}>
{authorAddress}
</div>
<div className="hidden md:block mt-1 text-[10px] text-white/65 leading-[1.2] truncate" title={fullTimestamp(item.created_at as unknown as string)}>
{compactTime(item.created_at as unknown as string)}
</div>
</>
unnamedHeader
)}

{/* Tokenized trend header */}
Expand Down
35 changes: 35 additions & 0 deletions src/features/social/components/useCompactFeedItemLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
useEffect, useLayoutEffect, useRef, useState,
} from 'react';

const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;

export function useCompactFeedItemLayout(threshold: number) {
const containerRef = useRef<HTMLElement | null>(null);
const [isCompact, setIsCompact] = useState(false);

useIsomorphicLayoutEffect(() => {
const node = containerRef.current;
if (!node) return () => {};

const update = (width: number) => {
setIsCompact(width > 0 && width < threshold);
};

update(node.getBoundingClientRect().width);

if (typeof ResizeObserver === 'function') {
const observer = new ResizeObserver((entries) => {
update(entries[0]?.contentRect.width ?? node.getBoundingClientRect().width);
});
observer.observe(node);
return () => observer.disconnect();
}

const handleResize = () => update(node.getBoundingClientRect().width);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [threshold]);

return { containerRef, isCompact };
}
Loading