Added comments to activities in the admin-x-activitypub app (#20958)

refs
[AP-279](https://linear.app/tryghost/issue/AP-279/handle-incoming-replies)

Wired up the comments to the activities in the admin-x-activitypub app
so that comments (replies) can be viewed by the user
This commit is contained in:
Michael Barrett 2024-09-11 17:44:12 +01:00 committed by GitHub
parent cbacea418f
commit 2cb9cf8b8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 102 additions and 48 deletions

View File

@ -4,7 +4,7 @@ import FeedItem from './feed/FeedItem';
import MainNavigation from './navigation/MainNavigation';
import NiceModal from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import {Activity} from './activities/ActivityItem';
import {type Activity} from './activities/ActivityItem';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Heading} from '@tryghost/admin-x-design-system';
import {useBrowseInboxForUser} from '../MainContent';
@ -12,25 +12,45 @@ import {useBrowseInboxForUser} from '../MainContent';
interface InboxProps {}
const Inbox: React.FC<InboxProps> = ({}) => {
const {data: activities = []} = useBrowseInboxForUser('index');
const [, setArticleContent] = useState<ObjectProperties | null>(null);
const [, setArticleActor] = useState<ActorProperties | null>(null);
const [layout, setLayout] = useState('inbox');
const inboxTabActivities = activities.filter((activity: Activity) => {
// Retrieve activities from the inbox
const {data: inboxActivities = []} = useBrowseInboxForUser('index');
const activities = inboxActivities.filter((activity: Activity) => {
const isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type);
const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note';
return isCreate || isAnnounce;
});
})
// API endpoint currently returns items oldest-newest, so reverse them
// to show the most recent activities first
.reverse();
const handleViewContent = (object: ObjectProperties, actor: ActorProperties) => {
// Create a map of activity comments, grouping them by the parent activity
// This allows us to quickly look up all comments for a given activity
const commentsMap = new Map<string, Activity[]>();
for (const activity of activities) {
if (activity.type === 'Create' && activity.object.inReplyTo) {
const comments = commentsMap.get(activity.object.inReplyTo) ?? [];
comments.push(activity);
commentsMap.set(activity.object.inReplyTo, comments.reverse());
}
}
const getCommentsForObject = (id: string) => {
return commentsMap.get(id) ?? [];
};
const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[]) => {
setArticleContent(object);
setArticleActor(actor);
NiceModal.show(ArticleModal, {
object: object,
actor: actor
});
NiceModal.show(ArticleModal, {object, actor, comments});
};
const handleLayoutChange = (newLayout: string) => {
@ -42,21 +62,26 @@ const Inbox: React.FC<InboxProps> = ({}) => {
<MainNavigation page='home' title="Home" onLayoutChange={handleLayoutChange} />
<div className='z-0 my-5 flex w-full flex-col'>
<div className='w-full'>
{inboxTabActivities.length > 0 ? (
{activities.length > 0 ? (
<ul className='mx-auto flex max-w-[640px] flex-col'>
{inboxTabActivities.reverse().map((activity, index) => (
{activities.map((activity, index) => (
<li
key={activity.id}
data-test-view-article
onClick={() => handleViewContent(activity.object, activity.actor)}
onClick={() => handleViewContent(
activity.object,
activity.actor,
getCommentsForObject(activity.object.id)
)}
>
<FeedItem
actor={activity.actor}
comments={getCommentsForObject(activity.object.id)}
layout={layout}
object={activity.object}
type={activity.type}
/>
{index < inboxTabActivities.length - 1 && (
{index < activities.length - 1 && (
<div className="h-px w-full bg-grey-200"></div>
)}
</li>

View File

@ -1,9 +1,12 @@
import React, {ReactNode} from 'react';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
export type Activity = {
type: string,
object: {
type: string
actor: ActorProperties,
object: ObjectProperties & {
inReplyTo: string | null // TODO: Move this to the ObjectProperties type
}
}

View File

@ -1,15 +1,20 @@
import FeedItem from './FeedItem';
import MainHeader from '../navigation/MainHeader';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useRef} from 'react';
import articleBodyStyles from '../articleBodyStyles';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Modal} from '@tryghost/admin-x-design-system';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import FeedItem from './FeedItem';
import MainHeader from '../navigation/MainHeader';
import articleBodyStyles from '../articleBodyStyles';
import {type Activity} from '../activities/ActivityItem';
interface ArticleModalProps {
object: ObjectProperties;
actor: ActorProperties;
comments: Activity[];
}
const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => {
@ -63,7 +68,7 @@ ${image &&
);
};
const ArticleModal: React.FC<ArticleModalProps> = ({object, actor}) => {
const ArticleModal: React.FC<ArticleModalProps> = ({object, actor, comments}) => {
const modal = useModal();
return (
<Modal
@ -89,20 +94,37 @@ const ArticleModal: React.FC<ArticleModalProps> = ({object, actor}) => {
<div className='mt-10 w-auto'>
{object.type === 'Note' && (
<div className='mx-auto max-w-[580px]'>
<FeedItem actor={actor} layout='modal' object={object} type='Note'/>
<FeedItem
actor={actor}
comments={comments}
layout='modal'
object={object}
type='Note'
/>
{/* {object.content && <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>} */}
{/* {renderAttachment(object)} */}
<FeedItem actor={actor} last={false} layout='reply' object={object} type='Note'/>
{/* <FeedItem actor={actor} last={false} layout='reply' object={object} type='Note'/>
<FeedItem actor={actor} last={true} layout='reply' object={object} type='Note'/>
<div className="mx-[-32px] my-4 h-px w-[120%] bg-grey-200"></div>
<FeedItem actor={actor} last={false} layout='reply' object={object} type='Note'/>
<FeedItem actor={actor} last={false} layout='reply' object={object} type='Note'/>
<FeedItem actor={actor} last={true} layout='reply' object={object} type='Note'/>
<FeedItem actor={actor} last={true} layout='reply' object={object} type='Note'/> */}
{comments.map((comment, index) => (
<FeedItem
actor={comment.actor}
last={index === comments.length - 1}
layout='reply'
object={comment.object}
type='Note'
/>
))}
</div>)}
{object.type === 'Article' && <ArticleBody heading={object.name} html={object.content} image={object?.image}/>}
{object.type === 'Article' && (
<ArticleBody heading={object.name} html={object.content} image={object?.image} />
)}
</div>
</Modal>
);
};
export default NiceModal.create(ArticleModal);
export default NiceModal.create(ArticleModal);

View File

@ -1,9 +1,12 @@
import APAvatar from '../global/APAvatar';
import React, {useState} from 'react';
import getRelativeTimestamp from '../../utils/get-relative-timestamp';
import getUsername from '../../utils/get-username';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Heading, Icon} from '@tryghost/admin-x-design-system';
import APAvatar from '../global/APAvatar';
import getRelativeTimestamp from '../../utils/get-relative-timestamp';
import getUsername from '../../utils/get-username';
import {type Activity} from '../activities/ActivityItem';
import {useLikeMutationForUser, useUnlikeMutationForUser} from '../../hooks/useActivityPubQueries';
export function renderFeedAttachment(object: ObjectProperties, layout: string) {
@ -155,13 +158,13 @@ const FeedItemStats: React.FC<{
return (<div className='flex gap-5'>
<div className='mt-3 flex gap-1'>
<Button
className={`self-start text-grey-900 transition-all hover:opacity-70 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`}
hideLabel={true}
icon='heart'
id='like'
size='md'
unstyled={true}
<Button
className={`self-start text-grey-900 transition-all hover:opacity-70 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`}
hideLabel={true}
icon='heart'
id='like'
size='md'
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
handleLikeClick();
@ -170,13 +173,13 @@ const FeedItemStats: React.FC<{
{isLiked && <span className={`text-grey-900`}>{likeCount}</span>}
</div>
<div className='mt-3 flex gap-1'>
<Button
className={`self-start text-grey-900`}
hideLabel={true}
icon='comment'
id='comment'
size='md'
unstyled={true}
<Button
className={`self-start text-grey-900`}
hideLabel={true}
icon='comment'
id='comment'
size='md'
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
onCommentClick();
@ -192,10 +195,11 @@ interface FeedItemProps {
object: ObjectProperties;
layout: string;
type: string;
comments?: Activity[];
last?: boolean;
}
const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last}) => {
const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comments = [], last}) => {
const timestamp =
new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'});
@ -238,7 +242,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
{renderFeedAttachment(object, layout)}
<FeedItemStats
commentCount={2}
commentCount={comments.length}
likeCount={1}
object={object}
onCommentClick={onLikeClick}
@ -283,7 +287,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.6rem] text-grey-900'></div>
{renderFeedAttachment(object, layout)}
<FeedItemStats
commentCount={2}
commentCount={comments.length}
likeCount={1}
object={object}
onCommentClick={onLikeClick}
@ -297,7 +301,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
</div>
<div className="mx-[-32px] my-4 h-px w-[120%] bg-grey-200"></div>
</div>
)}
</>
);
@ -330,7 +334,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
{renderFeedAttachment(object, layout)}
<FeedItemStats
commentCount={2}
commentCount={comments.length}
likeCount={1}
object={object}
onCommentClick={onLikeClick}
@ -367,7 +371,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
{renderInboxAttachment(object)}
</div>
<FeedItemStats
commentCount={2}
commentCount={comments.length}
likeCount={1}
object={object}
onCommentClick={onLikeClick}