mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 19:32:54 +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 MainNavigation from './navigation/MainNavigation';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import React, {useState} from '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 {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||||
import {Button, Heading} from '@tryghost/admin-x-design-system';
|
import {Button, Heading} from '@tryghost/admin-x-design-system';
|
||||||
import {useBrowseInboxForUser} from '../MainContent';
|
import {useBrowseInboxForUser} from '../MainContent';
|
||||||
@ -12,25 +12,45 @@ import {useBrowseInboxForUser} from '../MainContent';
|
|||||||
interface InboxProps {}
|
interface InboxProps {}
|
||||||
|
|
||||||
const Inbox: React.FC<InboxProps> = ({}) => {
|
const Inbox: React.FC<InboxProps> = ({}) => {
|
||||||
const {data: activities = []} = useBrowseInboxForUser('index');
|
|
||||||
const [, setArticleContent] = useState<ObjectProperties | null>(null);
|
const [, setArticleContent] = useState<ObjectProperties | null>(null);
|
||||||
const [, setArticleActor] = useState<ActorProperties | null>(null);
|
const [, setArticleActor] = useState<ActorProperties | null>(null);
|
||||||
const [layout, setLayout] = useState('inbox');
|
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 isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type);
|
||||||
const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note';
|
const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note';
|
||||||
|
|
||||||
return isCreate || isAnnounce;
|
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);
|
setArticleContent(object);
|
||||||
setArticleActor(actor);
|
setArticleActor(actor);
|
||||||
NiceModal.show(ArticleModal, {
|
NiceModal.show(ArticleModal, {object, actor, comments});
|
||||||
object: object,
|
|
||||||
actor: actor
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLayoutChange = (newLayout: string) => {
|
const handleLayoutChange = (newLayout: string) => {
|
||||||
@ -42,21 +62,26 @@ const Inbox: React.FC<InboxProps> = ({}) => {
|
|||||||
<MainNavigation page='home' title="Home" onLayoutChange={handleLayoutChange} />
|
<MainNavigation page='home' title="Home" onLayoutChange={handleLayoutChange} />
|
||||||
<div className='z-0 my-5 flex w-full flex-col'>
|
<div className='z-0 my-5 flex w-full flex-col'>
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
{inboxTabActivities.length > 0 ? (
|
{activities.length > 0 ? (
|
||||||
<ul className='mx-auto flex max-w-[640px] flex-col'>
|
<ul className='mx-auto flex max-w-[640px] flex-col'>
|
||||||
{inboxTabActivities.reverse().map((activity, index) => (
|
{activities.map((activity, index) => (
|
||||||
<li
|
<li
|
||||||
key={activity.id}
|
key={activity.id}
|
||||||
data-test-view-article
|
data-test-view-article
|
||||||
onClick={() => handleViewContent(activity.object, activity.actor)}
|
onClick={() => handleViewContent(
|
||||||
|
activity.object,
|
||||||
|
activity.actor,
|
||||||
|
getCommentsForObject(activity.object.id)
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<FeedItem
|
<FeedItem
|
||||||
actor={activity.actor}
|
actor={activity.actor}
|
||||||
|
comments={getCommentsForObject(activity.object.id)}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
object={activity.object}
|
object={activity.object}
|
||||||
type={activity.type}
|
type={activity.type}
|
||||||
/>
|
/>
|
||||||
{index < inboxTabActivities.length - 1 && (
|
{index < activities.length - 1 && (
|
||||||
<div className="h-px w-full bg-grey-200"></div>
|
<div className="h-px w-full bg-grey-200"></div>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import React, {ReactNode} from 'react';
|
import React, {ReactNode} from 'react';
|
||||||
|
|
||||||
|
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||||
|
|
||||||
export type Activity = {
|
export type Activity = {
|
||||||
type: string,
|
type: string,
|
||||||
object: {
|
actor: ActorProperties,
|
||||||
type: string
|
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 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 {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||||
import {Button, Modal} from '@tryghost/admin-x-design-system';
|
import {Button, Modal} from '@tryghost/admin-x-design-system';
|
||||||
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
|
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 {
|
interface ArticleModalProps {
|
||||||
object: ObjectProperties;
|
object: ObjectProperties;
|
||||||
actor: ActorProperties;
|
actor: ActorProperties;
|
||||||
|
comments: Activity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => {
|
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();
|
const modal = useModal();
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -89,20 +94,37 @@ const ArticleModal: React.FC<ArticleModalProps> = ({object, actor}) => {
|
|||||||
<div className='mt-10 w-auto'>
|
<div className='mt-10 w-auto'>
|
||||||
{object.type === 'Note' && (
|
{object.type === 'Note' && (
|
||||||
<div className='mx-auto max-w-[580px]'>
|
<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>} */}
|
{/* {object.content && <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>} */}
|
||||||
{/* {renderAttachment(object)} */}
|
{/* {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'/>
|
<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>
|
<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={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>)}
|
</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>
|
</div>
|
||||||
</Modal>
|
</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 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 {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||||
import {Button, Heading, Icon} from '@tryghost/admin-x-design-system';
|
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';
|
import {useLikeMutationForUser, useUnlikeMutationForUser} from '../../hooks/useActivityPubQueries';
|
||||||
|
|
||||||
export function renderFeedAttachment(object: ObjectProperties, layout: string) {
|
export function renderFeedAttachment(object: ObjectProperties, layout: string) {
|
||||||
@ -155,13 +158,13 @@ const FeedItemStats: React.FC<{
|
|||||||
|
|
||||||
return (<div className='flex gap-5'>
|
return (<div className='flex gap-5'>
|
||||||
<div className='mt-3 flex gap-1'>
|
<div className='mt-3 flex gap-1'>
|
||||||
<Button
|
<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' : ''}`}
|
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}
|
hideLabel={true}
|
||||||
icon='heart'
|
icon='heart'
|
||||||
id='like'
|
id='like'
|
||||||
size='md'
|
size='md'
|
||||||
unstyled={true}
|
unstyled={true}
|
||||||
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
handleLikeClick();
|
handleLikeClick();
|
||||||
@ -170,13 +173,13 @@ const FeedItemStats: React.FC<{
|
|||||||
{isLiked && <span className={`text-grey-900`}>{likeCount}</span>}
|
{isLiked && <span className={`text-grey-900`}>{likeCount}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-3 flex gap-1'>
|
<div className='mt-3 flex gap-1'>
|
||||||
<Button
|
<Button
|
||||||
className={`self-start text-grey-900`}
|
className={`self-start text-grey-900`}
|
||||||
hideLabel={true}
|
hideLabel={true}
|
||||||
icon='comment'
|
icon='comment'
|
||||||
id='comment'
|
id='comment'
|
||||||
size='md'
|
size='md'
|
||||||
unstyled={true}
|
unstyled={true}
|
||||||
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
||||||
e?.stopPropagation();
|
e?.stopPropagation();
|
||||||
onCommentClick();
|
onCommentClick();
|
||||||
@ -192,10 +195,11 @@ interface FeedItemProps {
|
|||||||
object: ObjectProperties;
|
object: ObjectProperties;
|
||||||
layout: string;
|
layout: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
comments?: Activity[];
|
||||||
last?: boolean;
|
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 =
|
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'});
|
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>
|
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
|
||||||
{renderFeedAttachment(object, layout)}
|
{renderFeedAttachment(object, layout)}
|
||||||
<FeedItemStats
|
<FeedItemStats
|
||||||
commentCount={2}
|
commentCount={comments.length}
|
||||||
likeCount={1}
|
likeCount={1}
|
||||||
object={object}
|
object={object}
|
||||||
onCommentClick={onLikeClick}
|
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>
|
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.6rem] text-grey-900'></div>
|
||||||
{renderFeedAttachment(object, layout)}
|
{renderFeedAttachment(object, layout)}
|
||||||
<FeedItemStats
|
<FeedItemStats
|
||||||
commentCount={2}
|
commentCount={comments.length}
|
||||||
likeCount={1}
|
likeCount={1}
|
||||||
object={object}
|
object={object}
|
||||||
onCommentClick={onLikeClick}
|
onCommentClick={onLikeClick}
|
||||||
@ -297,7 +301,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||||||
</div>
|
</div>
|
||||||
<div className="mx-[-32px] my-4 h-px w-[120%] bg-grey-200"></div>
|
<div className="mx-[-32px] my-4 h-px w-[120%] bg-grey-200"></div>
|
||||||
</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>
|
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
|
||||||
{renderFeedAttachment(object, layout)}
|
{renderFeedAttachment(object, layout)}
|
||||||
<FeedItemStats
|
<FeedItemStats
|
||||||
commentCount={2}
|
commentCount={comments.length}
|
||||||
likeCount={1}
|
likeCount={1}
|
||||||
object={object}
|
object={object}
|
||||||
onCommentClick={onLikeClick}
|
onCommentClick={onLikeClick}
|
||||||
@ -367,7 +371,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||||||
{renderInboxAttachment(object)}
|
{renderInboxAttachment(object)}
|
||||||
</div>
|
</div>
|
||||||
<FeedItemStats
|
<FeedItemStats
|
||||||
commentCount={2}
|
commentCount={comments.length}
|
||||||
likeCount={1}
|
likeCount={1}
|
||||||
object={object}
|
object={object}
|
||||||
onCommentClick={onLikeClick}
|
onCommentClick={onLikeClick}
|
||||||
|
Loading…
Reference in New Issue
Block a user