Skip to content
Open
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
6 changes: 5 additions & 1 deletion packages/shared/src/components/FeedItemComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { PlaceholderGrid } from './cards/placeholder/PlaceholderGrid';
import { PlaceholderList } from './cards/placeholder/PlaceholderList';
import { SignalPlaceholderList } from './cards/placeholder/SignalPlaceholderList';
import type { Ad, Post, PostItem } from '../graphql/posts';
import { PostType } from '../graphql/posts';
import { isXShareLikePost, PostType } from '../graphql/posts';
import type { LoggedUser } from '../lib/user';
import useLogImpression from '../hooks/feed/useLogImpression';
import type { FeedPostClick } from '../hooks/feed/useFeedOnPostClick';
Expand Down Expand Up @@ -136,6 +136,10 @@ const getPostTypeForCard = (post?: Post): PostType => {
return PostType.Article;
}

if (isXShareLikePost(post)) {
return PostType.SocialTwitter;
}

return post.type;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import { QueryClient } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { TestBootProvider } from '../../../../__tests__/helpers/boot';
import { sharePost } from '../../../../__tests__/fixture/post';
import type { Post } from '../../../graphql/posts';
import { PostType } from '../../../graphql/posts';
import { EmbeddedTweetPreview } from './EmbeddedTweetPreview';

const basePost: Post = {
...sharePost,
type: PostType.SocialTwitter,
title: 'Root tweet',
image: 'https://pbs.twimg.com/media/tweet.jpg',
sharedPost: undefined,
};

const avatarUser = {
id: 'user-1',
image: 'https://pbs.twimg.com/profile_images/user.jpg',
username: 'dailydotdev',
name: 'daily.dev',
};

describe('EmbeddedTweetPreview', () => {
it('always renders identity in ltr direction', () => {
render(
<TestBootProvider client={new QueryClient()}>
<EmbeddedTweetPreview
post={{ ...basePost, language: 'fa' }}
embeddedTweetAvatarUser={avatarUser}
embeddedTweetIdentity="Noojaan Farahmand @noojaanf"
textClampClass=""
/>
</TestBootProvider>,
);

expect(screen.getByText('Noojaan Farahmand @noojaanf')).toHaveAttribute(
'dir',
'ltr',
);
});

it('does not render placeholder image as tweet media', () => {
render(
<TestBootProvider client={new QueryClient()}>
<EmbeddedTweetPreview
post={{
...basePost,
image:
'https://media.daily.dev/image/upload/s--1KxV4ohY--/f_auto/v1722860400/public/Placeholder%2007',
}}
embeddedTweetAvatarUser={avatarUser}
embeddedTweetIdentity="daily.dev @dailydotdev"
textClampClass=""
showMedia
/>
</TestBootProvider>,
);

expect(screen.queryByAltText('Tweet media')).not.toBeInTheDocument();
});

it('renders tweet media when image is real', () => {
render(
<TestBootProvider client={new QueryClient()}>
<EmbeddedTweetPreview
post={basePost}
embeddedTweetAvatarUser={avatarUser}
embeddedTweetIdentity="daily.dev @dailydotdev"
textClampClass=""
showMedia
/>
</TestBootProvider>,
);

expect(screen.getByAltText('Tweet media')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import type { UserImageProps } from '../../ProfilePicture';
import { ProfileImageSize, ProfilePicture } from '../../ProfilePicture';
import { IconSize } from '../../Icon';
import { TwitterIcon } from '../../icons';
import { Image } from '../../image/Image';
import { isPlaceholderImage } from '../../../lib/image';
import { getSocialTextDirectionProps } from './socialTwitterHelpers';

interface EmbeddedTweetPreviewProps {
post: Post;
Expand All @@ -15,8 +18,28 @@ interface EmbeddedTweetPreviewProps {
textClampClass: string;
bodyClassName?: string;
showXLogo?: boolean;
showMedia?: boolean;
mediaContainerClassName?: string;
mediaClassName?: string;
}

const isLikelyTweetMediaUrl = (url?: string): boolean => {
if (!url) {
return false;
}

const normalized = url.toLowerCase();
if (
normalized.includes('/profile_images/') ||
normalized.includes('/profile_banners/') ||
isPlaceholderImage(url)
) {
return false;
}

return true;
};

export function EmbeddedTweetPreview({
post,
embeddedTweetAvatarUser,
Expand All @@ -25,30 +48,41 @@ export function EmbeddedTweetPreview({
textClampClass,
bodyClassName,
showXLogo = false,
showMedia = false,
mediaContainerClassName,
mediaClassName,
}: EmbeddedTweetPreviewProps): ReactElement {
const resolvedBodyClassName = bodyClassName ?? 'typo-callout';
const mediaSrc = post.sharedPost?.image || post.image;
const shouldShowMedia = showMedia && isLikelyTweetMediaUrl(mediaSrc);
const tweetLanguage = post.sharedPost?.language || post.language;
const tweetTextDirectionProps = getSocialTextDirectionProps(tweetLanguage);
const tweetBody = post.sharedPost?.title || post.title;
const tweetBodyHtml = post.sharedPost?.titleHtml || post.titleHtml;

return (
<div
className={classNames(
'rounded-12 border border-border-subtlest-tertiary p-3',
'overflow-hidden rounded-12 border border-border-subtlest-tertiary p-3',
className,
)}
>
<div className="flex min-w-0 items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-1">
<div className="flex min-w-0 items-center gap-2">
<div className="flex min-w-0 flex-1 items-center gap-1">
<ProfilePicture
user={embeddedTweetAvatarUser}
size={ProfileImageSize.Size16}
rounded="full"
className="shrink-0"
nativeLazyLoading
/>
<div className="min-w-0">
<div className="min-w-0 flex-1">
{!!embeddedTweetIdentity && (
<p
dir="ltr"
suppressHydrationWarning
className={classNames(
'truncate font-bold typo-caption1',
'w-full truncate font-bold typo-caption1',
post.read ? 'text-text-tertiary' : 'text-text-primary',
)}
>
Expand All @@ -59,33 +93,51 @@ export function EmbeddedTweetPreview({
</div>
{showXLogo && (
<TwitterIcon
className="shrink-0 text-text-tertiary"
className="ml-auto shrink-0 text-text-tertiary"
size={IconSize.XSmall}
/>
)}
</div>
{post.sharedPost?.titleHtml ? (
{tweetBodyHtml ? (
<p
{...tweetTextDirectionProps}
suppressHydrationWarning
className={classNames(
'mt-1 whitespace-pre-line break-words',
resolvedBodyClassName,
post.read ? 'text-text-tertiary' : 'text-text-primary',
textClampClass,
)}
dangerouslySetInnerHTML={{ __html: post.sharedPost.titleHtml }}
dangerouslySetInnerHTML={{ __html: tweetBodyHtml }}
/>
) : (
<p
{...tweetTextDirectionProps}
suppressHydrationWarning
className={classNames(
'mt-1 whitespace-pre-line break-words',
resolvedBodyClassName,
post.read ? 'text-text-tertiary' : 'text-text-primary',
textClampClass,
)}
>
{post.sharedPost?.title}
{tweetBody}
</p>
)}
{shouldShowMedia && !!mediaSrc && (
<div
className={classNames(
'mt-2 overflow-hidden rounded-12',
mediaContainerClassName,
)}
>
<Image
src={mediaSrc}
alt="Tweet media"
className={classNames('h-auto w-full object-cover', mediaClassName)}
/>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ it('should render top action link using post comments permalink', async () => {
expect(link).toHaveAttribute('href', basePost.permalink);
});

it('should render source name next to metadata date for regular tweets', async () => {
it('should render "From x.com" next to metadata date for regular tweets', async () => {
renderComponent();

expect(await screen.findByText(/Avengers/i)).toBeInTheDocument();
expect(await screen.findByText(/From x\.com/i)).toBeInTheDocument();
expect(screen.queryByText(/Avengers reposted/i)).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -127,7 +127,7 @@ it('should render quote/repost detail from shared post', async () => {
},
});

expect(await screen.findByText(/Avengers reposted/i)).toBeInTheDocument();
expect(await screen.findByText(/From x\.com/i)).toBeInTheDocument();
expect((await screen.findAllByText(/@devrelweekly/)).length).toBeGreaterThan(
0,
);
Expand Down Expand Up @@ -173,7 +173,7 @@ it('should use creatorTwitter when shared source is unknown', async () => {
expect(screen.queryByText('@unknown')).not.toBeInTheDocument();
});

it('should prefer source name when source id is unknown', async () => {
it('should use creator identity when source id is unknown', async () => {
renderComponent({
post: {
...basePost,
Expand All @@ -186,10 +186,7 @@ it('should prefer source name when source id is unknown', async () => {
},
});

expect(
await screen.findByText('Lee Hansel Solevilla Jr'),
).toBeInTheDocument();
expect(screen.queryByText('@root_creator')).not.toBeInTheDocument();
expect(await screen.findByText('root_creator')).toBeInTheDocument();
expect(screen.queryByText('@unknown')).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -221,7 +218,7 @@ it('should hide headline and tags for repost cards without repost text', async (
),
).not.toBeInTheDocument();
expect(screen.queryByTestId('post-tags')).not.toBeInTheDocument();
expect(await screen.findByText(/Avengers reposted/i)).toBeInTheDocument();
expect(await screen.findByText(/From x\.com/i)).toBeInTheDocument();
expect(
await screen.findByText(/Y Combinator @ycombinator/i),
).toBeInTheDocument();
Expand Down
Loading