From 78aff170bf089138a2a1bc4fee39d692978aa7f3 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Thu, 19 Sep 2024 18:07:14 +0100 Subject: [PATCH] Updated activitypub app in preparation for pagination refs [TryGhost/ActivityPub#44](https://github.com/TryGhost/ActivityPub/pull/44) To support pagination in the activitypub app, the following changes have been made: - Move filtering and sorting of activities to the server - Refactor how comments are processed (comments are now returned as part of the activity) --- .../src/api/activitypub.ts | 14 ++++- .../src/components/Activities.tsx | 59 ++++--------------- .../src/components/Inbox.tsx | 37 +++--------- .../components/activities/ActivityItem.tsx | 1 + .../src/components/feed/ArticleModal.tsx | 13 ++-- .../src/hooks/useActivityPubQueries.ts | 16 ++++- 6 files changed, 51 insertions(+), 89 deletions(-) diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index e5427ab0ee4..bb65f8ec033 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -147,7 +147,11 @@ export class ActivityPubAPI { return new URL(`.ghost/activitypub/activities/${this.handle}`, this.apiUrl); } - async getAllActivities(includeOwn: boolean = false): Promise { + async getAllActivities( + includeOwn: boolean = false, + includeReplies: boolean = false, + filter: {type?: string[]} | null = null + ): Promise { const LIMIT = 50; const fetchActivities = async (url: URL): Promise => { @@ -175,6 +179,10 @@ export class ActivityPubAPI { nextUrl.searchParams.set('cursor', json.nextCursor); nextUrl.searchParams.set('limit', LIMIT.toString()); nextUrl.searchParams.set('includeOwn', includeOwn.toString()); + nextUrl.searchParams.set('includeReplies', includeReplies.toString()); + if (filter) { + nextUrl.searchParams.set('filter', encodeURI(JSON.stringify(filter))); + } const nextItems = await fetchActivities(nextUrl); @@ -188,6 +196,10 @@ export class ActivityPubAPI { const url = new URL(this.activitiesApiUrl); url.searchParams.set('limit', LIMIT.toString()); url.searchParams.set('includeOwn', includeOwn.toString()); + url.searchParams.set('includeReplies', includeReplies.toString()); + if (filter) { + url.searchParams.set('filter', encodeURI(JSON.stringify(filter))); + } // Fetch the activities return fetchActivities(url); diff --git a/apps/admin-x-activitypub/src/components/Activities.tsx b/apps/admin-x-activitypub/src/components/Activities.tsx index f7e7966e65b..da4cd14fe79 100644 --- a/apps/admin-x-activitypub/src/components/Activities.tsx +++ b/apps/admin-x-activitypub/src/components/Activities.tsx @@ -87,10 +87,17 @@ const getActivityBadge = (activity: Activity): AvatarBadge => { const Activities: React.FC = ({}) => { const user = 'index'; - - let {data: activities = []} = useAllActivitiesForUser({handle: 'index', includeOwn: true}); const siteUrl = useSiteUrl(); + const {data: activities = []} = useAllActivitiesForUser({ + handle: user, + includeOwn: true, + includeReplies: true, + filter: { + type: ['Follow', 'Like', `Create:Note:isReplyToOwn,${new URL(siteUrl).hostname}`] + } + }); + // Create a map of activity objects from activities in the inbox and outbox. // This allows us to quickly look up an object associated with an activity // We could just make a http request to get the object, but this is more @@ -103,51 +110,6 @@ const Activities: React.FC = ({}) => { } }); - // Filter the activities to show - activities = activities.filter((activity) => { - if (activity.type === ACTVITY_TYPE.CREATE) { - // Only show "Create" activities that are replies to a post created - // by the user - - const replyToObject = activityObjectsMap.get(activity.object?.inReplyTo || ''); - - // If the reply object is not found, or it doesn't have a URL or - // name, do not show the activity - if (!replyToObject || !replyToObject.url || !replyToObject.name) { - return false; - } - - // Verify that the reply is to a post created by the user by - // checking that the hostname associated with the reply object - // is the same as the hostname of the site. This is not a bullet - // proof check, but it's a good enough for now - const hostname = new URL(siteUrl).hostname; - const replyToObjectHostname = new URL(replyToObject.url).hostname; - - return hostname === replyToObjectHostname; - } - - return [ACTVITY_TYPE.FOLLOW, ACTVITY_TYPE.LIKE].includes(activity.type); - }); - - // 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(); - - for (const activity of activities) { - if (activity.type === ACTVITY_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) ?? []; - }; - // Retrieve followers for the user const {data: followers = []} = useFollowersForUser(user); @@ -177,8 +139,7 @@ const Activities: React.FC = ({}) => { NiceModal.show(ArticleModal, { object: activity.object, actor: activity.actor, - comments: getCommentsForObject(activity.object.id), - allComments: commentsMap + comments: activity.object.replies }); } : undefined } diff --git a/apps/admin-x-activitypub/src/components/Inbox.tsx b/apps/admin-x-activitypub/src/components/Inbox.tsx index dbe4fa802d8..feff3b3b680 100644 --- a/apps/admin-x-activitypub/src/components/Inbox.tsx +++ b/apps/admin-x-activitypub/src/components/Inbox.tsx @@ -17,37 +17,18 @@ const Inbox: React.FC = ({}) => { const [layout, setLayout] = useState('inbox'); // Retrieve all activities for the user - let {data: activities = []} = useAllActivitiesForUser({handle: 'index'}); - - activities = activities.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; - }); - - // 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(); - - 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 {data: activities = []} = useAllActivitiesForUser({ + handle: 'index', + includeReplies: true, + filter: { + type: ['Create:Article', 'Create:Note', 'Announce:Note'] } - } - - const getCommentsForObject = (id: string) => { - return commentsMap.get(id) ?? []; - }; + }); const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[]) => { setArticleContent(object); setArticleActor(actor); - NiceModal.show(ArticleModal, {object, actor, comments, allComments: commentsMap}); + NiceModal.show(ArticleModal, {object, actor, comments}); }; function getContentAuthor(activity: Activity) { @@ -92,12 +73,12 @@ const Inbox: React.FC = ({}) => { onClick={() => handleViewContent( activity.object, getContentAuthor(activity), - getCommentsForObject(activity.object.id) + activity.object.replies )} > ; } const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => { @@ -73,7 +72,7 @@ const FeedItemDivider: React.FC = () => (
); -const ArticleModal: React.FC = ({object, actor, comments, allComments}) => { +const ArticleModal: React.FC = ({object, actor, comments}) => { const MODAL_SIZE_SM = 640; const MODAL_SIZE_LG = 1024; @@ -98,8 +97,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all modal.show({ object: previousObject, actor: previousActor, - comments: previousComments, - allComments: allComments + comments: previousComments }); }; const navigateForward = (nextObject: ObjectProperties, nextActor: ActorProperties, nextComments: Activity[]) => { @@ -109,8 +107,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all modal.show({ object: nextObject, actor: nextActor, - comments: nextComments, - allComments: allComments + comments: nextComments }); }; const toggleModalSize = () => { @@ -161,7 +158,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all */} {comments.map((comment, index) => { const showDivider = index !== comments.length - 1; - const nestedComments = allComments.get(comment.object.id) ?? []; + const nestedComments = comment.object.replies ?? []; const hasNestedComments = nestedComments.length > 0; return ( @@ -179,7 +176,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all /> {hasNestedComments && } {nestedComments.map((nestedComment, nestedCommentIndex) => { - const nestedNestedComments = allComments.get(nestedComment.object.id) ?? []; + const nestedNestedComments = nestedComment.object.replies ?? []; return (