This repository has no description
semble
/
src
/
webapp
/
features
/
platforms
/
bluesky
/
components
/
blueskySemblePost
/
BlueskySemblePost.tsx
3.9 kB
123 lines
1import { AppBskyFeedDefs, AppBskyFeedPost } from '@atproto/api';
2import { getBlueskyPost } from '../../lib/dal';
3import SembleHeader from '@/features/semble/components/SembleHeader/SembleHeader';
4import { getPostUriFromUrl } from '@/lib/utils/atproto';
5import RichTextRenderer from '@/components/contentDisplay/richTextRenderer/RichTextRenderer';
6import { detectUrlPlatform, getDomain, SupportedPlatform } from '@/lib/utils/link';
7import { getFormattedDate } from '@/lib/utils/time';
8import {
9 Stack,
10 Tooltip,
11 Anchor,
12 Card,
13 Group,
14 Avatar,
15 Box,
16 Image,
17 Text,
18} from '@mantine/core';
19import Link from 'next/link';
20import { FaBluesky } from 'react-icons/fa6';
21import PostEmbed from '../postEmbed/PostEmbed';
22import BlackskyLogo from '@/assets/icons/blacksky-logo.svg';
23import BlackskyLogoWhite from '@/assets/icons/blacksky-logo-white.svg';
24import { Suspense } from 'react';
25import UrlTypeBadge from '@/features/semble/components/urlTypeBadge/UrlTypeBadge';
26import UrlTypeBadgeSkeleton from '@/features/semble/components/urlTypeBadge/Skeleton.UrlTypeBadge';
27
28interface Props {
29 url: string;
30}
31
32export default async function BlueskySemblePost(props: Props) {
33 const postUri = getPostUriFromUrl(props.url);
34 const data = await getBlueskyPost(postUri);
35
36 const platform = detectUrlPlatform(props.url);
37 const platformIcon =
38 platform.type === SupportedPlatform.BLUESKY_POST ? (
39 <FaBluesky fill="#0085ff" size={18} />
40 ) : (
41 <>
42 <Image
43 src={BlackskyLogo.src}
44 alt="Blacksky logo"
45 w={18}
46 h={'auto'}
47 darkHidden
48 />
49 <Image
50 src={BlackskyLogoWhite.src}
51 alt="Blacksky logo"
52 w={18}
53 h={'auto'}
54 lightHidden
55 />
56 </>
57 );
58
59 if (
60 !data.thread ||
61 !AppBskyFeedDefs.isThreadViewPost(data.thread) ||
62 AppBskyFeedDefs.isNotFoundPost(data.thread.post) ||
63 AppBskyFeedDefs.isBlockedPost(data.thread.post)
64 ) {
65 // fallback
66 return <SembleHeader url={props.url} />;
67 }
68
69 const post = data.thread.post;
70 const record = post.record as AppBskyFeedPost.Record;
71
72 return (
73 <Stack gap={'xs'}>
74 <Suspense fallback={<UrlTypeBadgeSkeleton />}>
75 <UrlTypeBadge url={props.url} />
76 </Suspense>
77
78 {/* Post */}
79 <Card radius={'lg'} withBorder>
80 <Stack gap={'xs'}>
81 <Group gap="xs" justify="space-between" wrap="nowrap">
82 <Group gap={'xs'} wrap="nowrap">
83 <Avatar
84 component={Link}
85 href={`https://bsky.app/profile/${post.author.handle}`}
86 src={post.author.avatar?.replace('avatar', 'avatar_thumbnail')}
87 alt={`${post.author.handle} social preview image`}
88 radius="xl"
89 />
90 <Stack gap={0} flex={1}>
91 <Text c="bright" lineClamp={1} fw={500} w="fit-content">
92 {post.author.displayName || post.author.handle}
93 </Text>
94 <Text c="gray" lineClamp={1} w="fit-content">
95 @{post.author.handle}
96 </Text>
97 </Stack>
98 </Group>
99 <Tooltip
100 label={`View on ${platform.type === SupportedPlatform.BLUESKY_POST ? 'Bluesky' : 'Blacksky'}`}
101 >
102 <Anchor href={props.url} target="_blank">
103 {platformIcon}
104 </Anchor>
105 </Tooltip>
106 </Group>
107 <Stack gap={'xs'} w={'100%'}>
108 <Box>
109 <RichTextRenderer
110 text={record.text}
111 textProps={{ c: 'bright' }}
112 />
113 </Box>
114 {post.embed && <PostEmbed embed={post.embed} />}
115 </Stack>
116 <Text c={'gray'} fz={'sm'} fw={500}>
117 {getFormattedDate(post.indexedAt)}
118 </Text>
119 </Stack>
120 </Card>
121 </Stack>
122 );
123}