Added alternative layout for displaying ActivityPub articles (#20556)

ref https://linear.app/tryghost/issue/MOM-244/add-alternative-inboxfeed-design, https://linear.app/tryghost/issue/MOM-256/fix-typescript-not-being-able-to-resolve-the-type

- Added a toggle for switching to an alternative layout for displaying
articles in the inbox
- Fixed actor icons not showing up in the inbox
- Fixed Typescript not being able to resolve the type for images
- Added Jest to make tests run

---------

Co-authored-by: Princi Vershwal <vershwal.princi@gmail.com>
This commit is contained in:
Djordje Vlaisavljevic 2024-07-08 15:11:01 +01:00 committed by GitHub
parent 6e884b1a95
commit a6c6114e6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1057 additions and 69 deletions

View File

@ -36,10 +36,13 @@
"@testing-library/react": "14.1.0",
"@tryghost/admin-x-design-system": "0.0.0",
"@tryghost/admin-x-framework": "0.0.0",
"@types/jest": "29.5.12",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"jest": "29.7.0",
"react": "18.3.1",
"react-dom": "18.3.1"
"react-dom": "18.3.1",
"ts-jest": "29.1.5"
},
"nx": {
"targets": {

View File

@ -1,10 +1,9 @@
// import NiceModal from '@ebay/nice-modal-react';
// import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
import React, {useState} from 'react';
import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
import React, {useEffect, useRef, useState} from 'react';
import articleBodyStyles from './articleBodyStyles';
import getUsername from '../utils/get-username';
import {ActorProperties, ObjectProperties, useBrowseFollowersForUser, useBrowseFollowingForUser, useBrowseInboxForUser} from '@tryghost/admin-x-framework/api/activitypub';
import {Avatar, Button, Heading, List, ListItem, Page, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system';
import {Avatar, Button, ButtonGroup, Heading, List, ListItem, Page, SelectOption, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@ -33,6 +32,8 @@ const ActivityPubComponent: React.FC = () => {
setArticleContent(null);
};
const [selectedOption, setSelectedOption] = useState<SelectOption>({label: 'Inbox', value: 'inbox'});
const [selectedTab, setSelectedTab] = useState('inbox');
const tabs: ViewTab[] = [
@ -40,15 +41,15 @@ const ActivityPubComponent: React.FC = () => {
id: 'inbox',
title: 'Inbox',
contents: <div className='grid grid-cols-6 items-start gap-8'>
<ul className='order-2 col-span-6 flex flex-col lg:order-1 lg:col-span-4'>
<ul className={`order-2 col-span-6 flex flex-col pb-8 lg:order-1 ${selectedOption.value === 'inbox' ? 'lg:col-span-4' : 'lg:col-span-3'}`}>
{activities && activities.some(activity => activity.type === 'Create' && activity.object.type === 'Article') ? (activities.slice().reverse().map(activity => (
activity.type === 'Create' && activity.object.type === 'Article' &&
<li key={activity.id} data-test-view-article onClick={() => handleViewContent(activity.object, activity.actor)}>
<ObjectContentDisplay actor={activity.actor} object={activity.object}/>
<ObjectContentDisplay actor={activity.actor} layout={selectedOption.value} object={activity.object}/>
</li>
))) : <div className='flex items-center justify-center text-center'>
<div className='flex max-w-[32em] flex-col items-center justify-center gap-4'>
{/* <img alt='Ghost site logos' className='w-[220px]' src={ActivityPubWelcomeImage}/> */}
<img alt='Ghost site logos' className='w-[220px]' src={ActivityPubWelcomeImage}/>
<Heading className='text-balance' level={2}>Welcome to ActivityPub</Heading>
<p className='text-pretty text-grey-800'>Were so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.</p>
<p className='text-pretty text-grey-800'>You can see all of the users on the rightfind your favorite ones and give them a follow.</p>
@ -78,7 +79,7 @@ const ActivityPubComponent: React.FC = () => {
{activities && activities.slice().reverse().map(activity => (
activity.type === 'Create' && activity.object.type === 'Article' &&
<li key={activity.id} data-test-view-article onClick={() => handleViewContent(activity.object, activity.actor)}>
<ObjectContentDisplay actor={activity.actor} object={activity.object}/>
<ObjectContentDisplay actor={activity.actor} layout={selectedOption.value} object={activity.object} />
</li>
))}
</ul>
@ -91,6 +92,25 @@ const ActivityPubComponent: React.FC = () => {
<Page>
{!articleContent ? (
<ViewContainer
actions={[<ButtonGroup buttons={[
{
icon: 'listview',
size: 'sm',
iconColorClass: selectedOption.value === 'inbox' ? 'text-black' : 'text-grey-500',
onClick: () => {
setSelectedOption({label: 'Inbox', value: 'inbox'});
}
},
{
icon: 'cardview',
size: 'sm',
iconColorClass: selectedOption.value === 'feed' ? 'text-black' : 'text-grey-500',
onClick: () => {
setSelectedOption({label: 'Feed', value: 'feed'});
}
}
]} clearBg={false} link outlineOnMobile />]}
firstOnPage={true}
primaryAction={{
title: 'Follow',
@ -102,9 +122,10 @@ const ActivityPubComponent: React.FC = () => {
selectedTab={selectedTab}
stickyHeader={true}
tabs={tabs}
toolbarBorder={false}
type='page'
onTabChange={setSelectedTab}
title='ActivityPub'
toolbarBorder={true}
type='page'
onTabChange={setSelectedTab}
>
</ViewContainer>
@ -117,7 +138,7 @@ const ActivityPubComponent: React.FC = () => {
};
const Sidebar: React.FC<{followingCount: number, followersCount: number, updateRoute: (route: string) => void}> = ({followingCount, followersCount, updateRoute}) => (
<div className='order-1 col-span-6 flex flex-col gap-5 lg:order-2 lg:col-span-2'>
<div className='order-1 col-span-6 flex flex-col gap-5 lg:order-2 lg:col-span-2 lg:col-start-5'>
<div className='rounded-xl bg-grey-50 p-6' id="ap-sidebar">
<div className='mb-4 border-b border-b-grey-200 pb-4'><SettingValue key={'your-username'} heading={'Your username'} value={'@index@localplaceholder.com'}/></div>
<div className='grid grid-cols-2 gap-4'>
@ -146,17 +167,17 @@ const Sidebar: React.FC<{followingCount: number, followersCount: number, updateR
);
const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => {
// const dangerouslySetInnerHTML = {__html: html};
// const cssFile = '../index.css';
const site = useBrowseSite();
const siteData = site.data?.site;
const iframeRef = useRef<HTMLIFrameElement>(null);
const cssContent = articleBodyStyles(siteData?.url.replace(/\/$/, ''));
const htmlContent = `
<html>
<head>
${cssContent}
${cssContent}
</head>
<body>
<header class="gh-article-header gh-canvas">
@ -174,20 +195,29 @@ ${image &&
</html>
`;
useEffect(() => {
const iframe = iframeRef.current;
if (iframe) {
iframe.srcdoc = htmlContent;
}
}, [htmlContent]);
return (
<iframe
className='h-[calc(100vh_-_3vmin_-_4.8rem_-_2px)]'
height="100%"
id="gh-ap-article-iframe"
srcDoc={htmlContent}
title="Embedded Content"
width="100%"
>
</iframe>
<div>
<iframe
ref={iframeRef}
className={`h-[calc(100vh_-_3vmin_-_4.8rem_-_2px)]`}
height="100%"
id="gh-ap-article-iframe"
title="Embedded Content"
width="100%"
>
</iframe>
</div>
);
};
const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties }> = ({actor, object}) => {
const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties, layout: string }> = ({actor, object, layout}) => {
const parser = new DOMParser();
const doc = parser.parseFromString(object.content || '', 'text/html');
@ -205,37 +235,75 @@ const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProp
setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms
};
return (
<>
{object && (
<div className='border-1 group/article relative z-10 flex cursor-pointer flex-col items-start justify-between border-b border-b-grey-200 py-5' data-test-activity>
<div className='relative z-10 mb-3 grid w-full grid-cols-[20px_auto_1fr_auto] items-center gap-2 text-base'>
<img className='w-5' src={actor.icon}/>
<span className='truncate font-semibold'>{actor.name}</span>
<span className='truncate text-grey-800'>{getUsername(actor)}</span>
<span className='ml-auto text-right text-grey-800'>{timestamp}</span>
</div>
<div className='relative z-10 grid w-full grid-cols-[auto_170px] gap-4'>
<div className='flex flex-col'>
<div className='flex w-full justify-between gap-4'>
<Heading className='mb-2 line-clamp-2 leading-tight' level={5} data-test-activity-heading>{object.name}</Heading>
</div>
<p className='mb-6 line-clamp-2 max-w-prose text-md text-grey-800'>{plainTextContent}</p>
<div className='flex gap-2'>
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id="like" size='md' unstyled={true} onClick={handleLikeClick}/>
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
if (layout === 'feed') {
return (
<>
{object && (
<div className='border-1 group/article relative z-10 flex cursor-pointer flex-col items-start justify-between border-b border-b-grey-200 py-6' data-test-activity>
<div className='relative z-10 mb-3 flex w-full items-center gap-3'>
<img className='w-8' src={actor.icon.url}/>
<div>
<p className='text-base font-bold' data-test-activity-heading>{actor.name}</p>
<div className='*:text-base *:text-grey-900'>
{/* <span className='truncate before:mx-1 before:content-["·"]'>{getUsername(actor)}</span> */}
<span>{timestamp}</span>
</div>
</div>
</div>
{object.image && <div className='relative min-w-[33%] grow'>
<img className='absolute h-full w-full rounded object-cover' src={object.image}/>
</div>}
<div className='relative z-10 w-full gap-4'>
<div className='flex flex-col'>
{object.image && <div className='relative mb-4'>
<img className='h-[300px] w-full rounded object-cover' src={object.image}/>
</div>}
<Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>
<p className='mb-4 line-clamp-3 max-w-prose text-pretty text-md text-grey-900'>{plainTextContent}</p>
<div className='flex gap-2'>
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id="like" size='md' unstyled={true} onClick={handleLikeClick}/>
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
</div>
</div>
</div>
<div className='absolute -inset-x-3 -inset-y-1 z-0 rounded transition-colors group-hover/article:bg-grey-100'></div>
{/* <div className='absolute inset-0 z-0 rounded from-white to-grey-50 transition-colors group-hover/article:bg-gradient-to-r'></div> */}
</div>
<div className='absolute -inset-x-3 inset-y-0 z-0 rounded transition-colors group-hover/article:bg-grey-50'></div>
{/* <div className='absolute inset-0 z-0 rounded from-white to-grey-50 transition-colors group-hover/article:bg-gradient-to-r'></div> */}
</div>
)}
</>
);
)}
</>
);
} else if (layout === 'inbox') {
return (
<>
{object && (
<div className='border-1 group/article relative z-10 flex cursor-pointer flex-col items-start justify-between border-b border-b-grey-200 py-5' data-test-activity>
<div className='relative z-10 mb-3 grid w-full grid-cols-[20px_auto_1fr_auto] items-center gap-2 text-base'>
<img className='w-5' src={actor.icon.url}/>
<span className='truncate font-semibold'>{actor.name}</span>
{/* <span className='truncate text-grey-800'>{getUsername(actor)}</span> */}
<span className='ml-auto text-right text-grey-800'>{timestamp}</span>
</div>
<div className='relative z-10 grid w-full grid-cols-[auto_170px] gap-4'>
<div className='flex flex-col'>
<div className='flex w-full justify-between gap-4'>
<Heading className='mb-1 line-clamp-2 leading-tight' level={5} data-test-activity-heading>{object.name}</Heading>
</div>
<p className='mb-6 line-clamp-2 max-w-prose text-pretty text-md text-grey-800'>{object.preview?.content}</p>
<div className='flex gap-2'>
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id="like" size='md' unstyled={true} onClick={handleLikeClick}/>
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
</div>
</div>
{object.image && <div className='relative min-w-[33%] grow'>
<img className='absolute h-full w-full rounded object-cover' height='140px' src={object.image} width='170px'/>
</div>}
</div>
<div className='absolute -inset-x-3 -inset-y-1 z-0 rounded transition-colors group-hover/article:bg-grey-50'></div>
{/* <div className='absolute inset-0 z-0 rounded from-white to-grey-50 transition-colors group-hover/article:bg-gradient-to-r'></div> */}
</div>
)}
</>
);
}
};
const ViewArticle: React.FC<ViewArticleProps> = ({object, onBackToList}) => {

View File

@ -4,6 +4,7 @@
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
"types": ["vite/client", "jest"],
/* Bundler mode */
"moduleResolution": "bundler",

946
yarn.lock

File diff suppressed because it is too large Load Diff