mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 11:16:01 +03:00
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:
parent
cbacea418f
commit
2cb9cf8b8b
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user