Preserve embedded original posts when rendering swarm reposts
Hop-State: A_06FPEZ3WB9ZANP4K6YVY4S0 Hop-Proposal: R_06FPEZ34Y13T1DBP9HTRMBR Hop-Task: T_06FPEYF7BR1M5HZ8Z1684KG Hop-Attempt: AT_06FPEYF7BS5KHWEJRTK2PW0
This commit is contained in:
@@ -10,6 +10,7 @@ import { shouldIncludeNsfwFeed } from '@/lib/nsfw/feed-access';
|
||||
import { isLocalNodeNsfw } from '@/lib/node/local-node';
|
||||
import { hasPublishablePostContent } from '@/lib/posts/content-policy';
|
||||
import { decodeFeedCursor, encodeFeedCursor } from '@/lib/posts/feed-pagination';
|
||||
import { mapSwarmPostToPost } from '@/lib/swarm/feed-post';
|
||||
|
||||
const POST_MAX_LENGTH = 600;
|
||||
const CURATION_WINDOW_HOURS = 72;
|
||||
@@ -759,46 +760,7 @@ export async function GET(request: Request) {
|
||||
includeNsfw,
|
||||
});
|
||||
|
||||
const mapSwarmPostToFeedPost = (sp: (typeof swarmResult.posts)[number]): any => ({
|
||||
id: `swarm:${sp.nodeDomain}:${sp.id}`,
|
||||
originalPostId: sp.id, // Keep the original ID for replies
|
||||
content: sp.content,
|
||||
createdAt: new Date(sp.createdAt),
|
||||
likesCount: sp.likeCount,
|
||||
repostsCount: sp.repostCount,
|
||||
repliesCount: sp.replyCount,
|
||||
isSwarm: true,
|
||||
nodeDomain: sp.nodeDomain,
|
||||
repostOfId: sp.repostOfId ? `swarm:${sp.nodeDomain}:${sp.repostOfId}` : null,
|
||||
repostOf: sp.repostOf ? mapSwarmPostToFeedPost(sp.repostOf) : null,
|
||||
author: {
|
||||
id: `swarm:${sp.nodeDomain}:${sp.author.handle}`,
|
||||
handle: sp.author.handle,
|
||||
displayName: sp.author.displayName,
|
||||
avatarUrl: sp.author.avatarUrl,
|
||||
isSwarm: true,
|
||||
isBot: sp.author.isBot,
|
||||
isNsfw: sp.author.isNsfw,
|
||||
nodeIsNsfw: sp.nodeIsNsfw,
|
||||
nodeDomain: sp.nodeDomain,
|
||||
},
|
||||
media: sp.media?.map((m, idx) => ({
|
||||
id: `swarm:${sp.nodeDomain}:${sp.id}:media:${idx}`,
|
||||
url: m.url,
|
||||
altText: m.altText || null,
|
||||
mimeType: m.mimeType || null,
|
||||
})) || [],
|
||||
linkPreviewUrl: sp.linkPreviewUrl || null,
|
||||
linkPreviewTitle: sp.linkPreviewTitle || null,
|
||||
linkPreviewDescription: sp.linkPreviewDescription || null,
|
||||
linkPreviewImage: sp.linkPreviewImage || null,
|
||||
linkPreviewType: sp.linkPreviewType || null,
|
||||
linkPreviewVideoUrl: sp.linkPreviewVideoUrl || null,
|
||||
linkPreviewMedia: sp.linkPreviewMedia || null,
|
||||
replyTo: null,
|
||||
});
|
||||
|
||||
const swarmPosts = swarmResult.posts.map(mapSwarmPostToFeedPost);
|
||||
const swarmPosts = swarmResult.posts.map(mapSwarmPostToPost);
|
||||
|
||||
let mutedIds = new Set<string>();
|
||||
let blockedIds = new Set<string>();
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useAuth } from '@/lib/contexts/AuthContext';
|
||||
import { signedAPI } from '@/lib/api/signed-fetch';
|
||||
import { AvatarImage } from '@/components/AvatarImage';
|
||||
import { useRuntimeConfig } from '@/lib/contexts/ConfigContext';
|
||||
import { mapSwarmPostToPost, type InteractiveSwarmPost } from '@/lib/swarm/feed-post';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -61,36 +62,13 @@ function UserCard({ user }: { user: User }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface SwarmPost {
|
||||
id: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
author: {
|
||||
handle: string;
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
isNsfw: boolean;
|
||||
};
|
||||
nodeDomain: string;
|
||||
nodeIsNsfw: boolean;
|
||||
likeCount: number;
|
||||
repostCount: number;
|
||||
replyCount: number;
|
||||
media?: { url: string; mimeType?: string; altText?: string }[];
|
||||
linkPreviewUrl?: string;
|
||||
linkPreviewTitle?: string;
|
||||
linkPreviewDescription?: string;
|
||||
linkPreviewImage?: string;
|
||||
isLiked?: boolean;
|
||||
}
|
||||
|
||||
export default function ExplorePage() {
|
||||
const { user, did, handle } = useAuth();
|
||||
const { config } = useRuntimeConfig();
|
||||
const [query, setQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'node' | 'swarm' | 'users' | 'search'>('node');
|
||||
const [nodePosts, setNodePosts] = useState<Post[]>([]);
|
||||
const [swarmPosts, setSwarmPosts] = useState<SwarmPost[]>([]);
|
||||
const [swarmPosts, setSwarmPosts] = useState<InteractiveSwarmPost[]>([]);
|
||||
const [swarmSources, setSwarmSources] = useState<{ domain: string; postCount: number }[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [searchResults, setSearchResults] = useState<{ posts: Post[]; users: User[] }>({ posts: [], users: [] });
|
||||
@@ -433,37 +411,7 @@ export default function ExplorePage() {
|
||||
</div>
|
||||
<div className="explore-posts">
|
||||
{swarmPosts.map((post) => {
|
||||
// Transform swarm post to Post format for PostCard
|
||||
const transformedPost: Post = {
|
||||
id: `swarm:${post.nodeDomain}:${post.id}`,
|
||||
originalPostId: post.id,
|
||||
content: post.content,
|
||||
createdAt: post.createdAt,
|
||||
likesCount: post.likeCount,
|
||||
repostsCount: post.repostCount,
|
||||
repliesCount: post.replyCount,
|
||||
isSwarm: true,
|
||||
nodeDomain: post.nodeDomain,
|
||||
author: {
|
||||
id: `swarm:${post.nodeDomain}:${post.author.handle}`,
|
||||
handle: post.author.handle,
|
||||
displayName: post.author.displayName,
|
||||
avatarUrl: post.author.avatarUrl,
|
||||
isNsfw: post.author.isNsfw,
|
||||
nodeIsNsfw: post.nodeIsNsfw,
|
||||
},
|
||||
media: post.media?.map((m, idx) => ({
|
||||
id: `swarm:${post.nodeDomain}:${post.id}:media:${idx}`,
|
||||
url: m.url,
|
||||
altText: m.altText || null,
|
||||
mimeType: m.mimeType || null,
|
||||
})) || [],
|
||||
linkPreviewUrl: post.linkPreviewUrl || null,
|
||||
linkPreviewTitle: post.linkPreviewTitle || null,
|
||||
linkPreviewDescription: post.linkPreviewDescription || null,
|
||||
linkPreviewImage: post.linkPreviewImage || null,
|
||||
isLiked: post.isLiked || false,
|
||||
};
|
||||
const transformedPost = mapSwarmPostToPost(post);
|
||||
return (
|
||||
<PostCard
|
||||
key={`${post.nodeDomain}:${post.id}`}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { SwarmPost } from '@/app/api/swarm/timeline/route';
|
||||
import { mapSwarmPostToPost } from './feed-post';
|
||||
|
||||
const original: SwarmPost = {
|
||||
id: 'original-id',
|
||||
content: 'Original post body',
|
||||
createdAt: '2026-07-15T18:49:15.000Z',
|
||||
author: {
|
||||
handle: 'original-author',
|
||||
displayName: 'Original Author',
|
||||
avatarUrl: 'https://example.com/original.png',
|
||||
isNsfw: false,
|
||||
},
|
||||
nodeDomain: 'remote.example',
|
||||
nodeIsNsfw: true,
|
||||
isNsfw: false,
|
||||
likeCount: 2,
|
||||
repostCount: 1,
|
||||
replyCount: 0,
|
||||
media: [{ url: 'https://example.com/image.jpg', mimeType: 'image/jpeg' }],
|
||||
};
|
||||
|
||||
describe('mapSwarmPostToPost', () => {
|
||||
it('preserves the complete embedded original when mapping a repost', () => {
|
||||
const repost: SwarmPost = {
|
||||
id: 'repost-id',
|
||||
content: '',
|
||||
createdAt: '2026-07-15T20:07:48.000Z',
|
||||
repostOfId: original.id,
|
||||
repostOf: original,
|
||||
author: {
|
||||
handle: 'reposter',
|
||||
displayName: 'Reposter',
|
||||
isNsfw: false,
|
||||
},
|
||||
nodeDomain: 'remote.example',
|
||||
nodeIsNsfw: true,
|
||||
isNsfw: false,
|
||||
likeCount: 0,
|
||||
repostCount: 0,
|
||||
replyCount: 0,
|
||||
};
|
||||
|
||||
const mapped = mapSwarmPostToPost(repost);
|
||||
|
||||
expect(mapped.repostOfId).toBe('swarm:remote.example:original-id');
|
||||
expect(mapped.repostOf).toMatchObject({
|
||||
id: 'swarm:remote.example:original-id',
|
||||
content: 'Original post body',
|
||||
nodeDomain: 'remote.example',
|
||||
author: {
|
||||
handle: 'original-author',
|
||||
nodeIsNsfw: true,
|
||||
},
|
||||
media: [{
|
||||
id: 'swarm:remote.example:original-id:media:0',
|
||||
url: 'https://example.com/image.jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves interaction and link-preview fields', () => {
|
||||
const mapped = mapSwarmPostToPost({
|
||||
...original,
|
||||
isLiked: true,
|
||||
isReposted: true,
|
||||
linkPreviewUrl: 'https://example.com/story',
|
||||
linkPreviewTitle: 'Story',
|
||||
linkPreviewType: 'card',
|
||||
});
|
||||
|
||||
expect(mapped).toMatchObject({
|
||||
isLiked: true,
|
||||
isReposted: true,
|
||||
linkPreviewUrl: 'https://example.com/story',
|
||||
linkPreviewTitle: 'Story',
|
||||
linkPreviewType: 'card',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { SwarmPost } from '@/app/api/swarm/timeline/route';
|
||||
import type { Post } from '@/lib/types';
|
||||
|
||||
export type InteractiveSwarmPost = SwarmPost & {
|
||||
isLiked?: boolean;
|
||||
isReposted?: boolean;
|
||||
};
|
||||
|
||||
export function mapSwarmPostToPost(post: InteractiveSwarmPost): Post {
|
||||
const normalizedId = `swarm:${post.nodeDomain}:${post.id}`;
|
||||
|
||||
return {
|
||||
id: normalizedId,
|
||||
originalPostId: post.id,
|
||||
content: post.content,
|
||||
createdAt: post.createdAt,
|
||||
likesCount: post.likeCount,
|
||||
repostsCount: post.repostCount,
|
||||
repliesCount: post.replyCount,
|
||||
isSwarm: true,
|
||||
nodeDomain: post.nodeDomain,
|
||||
repostOfId: post.repostOfId
|
||||
? `swarm:${post.nodeDomain}:${post.repostOfId}`
|
||||
: null,
|
||||
repostOf: post.repostOf
|
||||
? mapSwarmPostToPost(post.repostOf as InteractiveSwarmPost)
|
||||
: null,
|
||||
author: {
|
||||
id: `swarm:${post.nodeDomain}:${post.author.handle}`,
|
||||
handle: post.author.handle,
|
||||
displayName: post.author.displayName,
|
||||
avatarUrl: post.author.avatarUrl,
|
||||
isSwarm: true,
|
||||
isBot: post.author.isBot,
|
||||
isNsfw: post.author.isNsfw,
|
||||
nodeIsNsfw: post.nodeIsNsfw,
|
||||
nodeDomain: post.nodeDomain,
|
||||
},
|
||||
media: post.media?.map((item, index) => ({
|
||||
id: `${normalizedId}:media:${index}`,
|
||||
url: item.url,
|
||||
altText: item.altText || null,
|
||||
mimeType: item.mimeType || null,
|
||||
})) || [],
|
||||
linkPreviewUrl: post.linkPreviewUrl || null,
|
||||
linkPreviewTitle: post.linkPreviewTitle || null,
|
||||
linkPreviewDescription: post.linkPreviewDescription || null,
|
||||
linkPreviewImage: post.linkPreviewImage || null,
|
||||
linkPreviewType: post.linkPreviewType || null,
|
||||
linkPreviewVideoUrl: post.linkPreviewVideoUrl || null,
|
||||
linkPreviewMedia: post.linkPreviewMedia || null,
|
||||
isLiked: post.isLiked || false,
|
||||
isReposted: post.isReposted || false,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user