0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-04 02:01:58 -05:00

Updated activities tab to use the activities endpoint in the activitypub app (#21037)

refs
[AP-377](https://linear.app/tryghost/issue/AP-377/inbox-returning-33mb-of-data),
[TryGhost/ActivityPub#43](https://github.com/TryGhost/ActivityPub/pull/43)

Updated activities tab to use the activities endpoint in the activitypub
app
This commit is contained in:
Michael Barrett 2024-09-19 10:45:54 +01:00 committed by GitHub
parent f9bb2d0ba7
commit a886d22437
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 50 additions and 65 deletions

View file

@ -24,23 +24,6 @@ export function useBrowseInboxForUser(handle: string) {
});
}
export function useBrowseOutboxForUser(handle: string) {
const site = useBrowseSite();
const siteData = site.data?.site;
const siteUrl = siteData?.url ?? window.location.origin;
const api = new ActivityPubAPI(
new URL(siteUrl),
new URL('/ghost/api/admin/identities/', window.location.origin),
handle
);
return useQuery({
queryKey: [`outbox:${handle}`],
async queryFn() {
return api.getOutbox();
}
});
}
export function useFollowersForUser(handle: string) {
const site = useBrowseSite();
const siteData = site.data?.site;

View file

@ -454,13 +454,13 @@ describe('ActivityPubAPI', function () {
}]
})
},
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50': {
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&includeOwn=false': {
response: JSONResponse({
items: [{type: 'Create', object: {type: 'Note'}}],
nextCursor: 'next-cursor'
})
},
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&cursor=next-cursor': {
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&includeOwn=false&cursor=next-cursor': {
response: JSONResponse({
items: [{type: 'Announce', object: {type: 'Article'}}],
nextCursor: null
@ -483,5 +483,37 @@ describe('ActivityPubAPI', function () {
expect(actual).toEqual(expected);
});
test('It fetches a user\'s own activities', async function () {
const fakeFetch = Fetch({
'https://auth.api/': {
response: JSONResponse({
identities: [{
token: 'fake-token'
}]
})
},
'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&includeOwn=true': {
response: JSONResponse({
items: [{type: 'Create', object: {type: 'Note'}}],
nextCursor: null
})
}
});
const api = new ActivityPubAPI(
new URL('https://activitypub.api'),
new URL('https://auth.api'),
'index',
fakeFetch
);
const actual = await api.getAllActivities(true);
const expected: Activity[] = [
{type: 'Create', object: {type: 'Note'}}
];
expect(actual).toEqual(expected);
});
});
});

View file

@ -53,24 +53,6 @@ export class ActivityPubAPI {
return [];
}
get outboxApiUrl() {
return new URL(`.ghost/activitypub/outbox/${this.handle}`, this.apiUrl);
}
async getOutbox(): Promise<Activity[]> {
const json = await this.fetchJSON(this.outboxApiUrl);
if (json === null) {
return [];
}
if ('orderedItems' in json) {
return Array.isArray(json.orderedItems) ? json.orderedItems : [json.orderedItems];
}
if ('items' in json) {
return Array.isArray(json.items) ? json.items : [json.items];
}
return [];
}
get followingApiUrl() {
return new URL(`.ghost/activitypub/following/${this.handle}`, this.apiUrl);
}
@ -165,7 +147,7 @@ export class ActivityPubAPI {
return new URL(`.ghost/activitypub/activities/${this.handle}`, this.apiUrl);
}
async getAllActivities(): Promise<Activity[]> {
async getAllActivities(includeOwn: boolean = false): Promise<Activity[]> {
const LIMIT = 50;
const fetchActivities = async (url: URL): Promise<Activity[]> => {
@ -192,6 +174,7 @@ export class ActivityPubAPI {
nextUrl.searchParams.set('cursor', json.nextCursor);
nextUrl.searchParams.set('limit', LIMIT.toString());
nextUrl.searchParams.set('includeOwn', includeOwn.toString());
const nextItems = await fetchActivities(nextUrl);
@ -204,6 +187,7 @@ export class ActivityPubAPI {
// Make a copy of the activities API URL and set the limit
const url = new URL(this.activitiesApiUrl);
url.searchParams.set('limit', LIMIT.toString());
url.searchParams.set('includeOwn', includeOwn.toString());
// Fetch the activities
return fetchActivities(url);

View file

@ -9,8 +9,8 @@ import ArticleModal from './feed/ArticleModal';
import MainNavigation from './navigation/MainNavigation';
import getUsername from '../utils/get-username';
import {useBrowseInboxForUser, useBrowseOutboxForUser, useFollowersForUser} from '../MainContent';
import {useSiteUrl} from '../hooks/useActivityPubQueries';
import {useAllActivitiesForUser, useSiteUrl} from '../hooks/useActivityPubQueries';
import {useFollowersForUser} from '../MainContent';
interface ActivitiesProps {}
@ -88,13 +88,7 @@ const getActivityBadge = (activity: Activity): AvatarBadge => {
const Activities: React.FC<ActivitiesProps> = ({}) => {
const user = 'index';
// Retrieve activities from the inbox AND the outbox
// Why the need for the outbox? The outbox contains activities that the user
// has performed, and we sometimes need information about the object
// associated with the activity (i.e when displaying the name of an article
// that a reply was made to)
const {data: inboxActivities = []} = useBrowseInboxForUser(user);
const {data: outboxActivities = []} = useBrowseOutboxForUser(user);
let {data: activities = []} = useAllActivitiesForUser({handle: 'index', includeOwn: true});
const siteUrl = useSiteUrl();
// Create a map of activity objects from activities in the inbox and outbox.
@ -103,22 +97,18 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
// efficient seeming though we already have the data in the inbox and outbox
const activityObjectsMap = new Map<string, ObjectProperties>();
outboxActivities.forEach((activity) => {
if (activity.object) {
activityObjectsMap.set(activity.object.id, activity.object);
}
});
inboxActivities.forEach((activity) => {
activities.forEach((activity) => {
if (activity.object) {
activityObjectsMap.set(activity.object.id, activity.object);
}
});
// Filter the activities to show
const activities = inboxActivities.filter((activity) => {
// Only show "Create" activities that are replies to a post created
// by the user
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
@ -138,10 +128,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
}
return [ACTVITY_TYPE.FOLLOW, ACTVITY_TYPE.LIKE].includes(activity.type);
})
// API endpoint currently returns items oldest-newest, so reverse them
// to show the most recent activities first
.reverse();
});
// 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

View file

@ -17,7 +17,7 @@ const Inbox: React.FC<InboxProps> = ({}) => {
const [layout, setLayout] = useState('inbox');
// Retrieve all activities for the user
let {data: activities = []} = useAllActivitiesForUser('index');
let {data: activities = []} = useAllActivitiesForUser({handle: 'index'});
activities = activities.filter((activity: Activity) => {
const isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type);

View file

@ -157,7 +157,6 @@ export function useFollowingForUser(handle: string) {
export function useFollowersForUser(handle: string) {
const siteUrl = useSiteUrl();
const api = createActivityPubAPI(handle, siteUrl);
return useQuery({
queryKey: [`followers:${handle}`],
async queryFn() {
@ -166,13 +165,13 @@ export function useFollowersForUser(handle: string) {
});
}
export function useAllActivitiesForUser(handle: string) {
export function useAllActivitiesForUser({handle, includeOwn = false}: {handle: string, includeOwn?: boolean}) {
const siteUrl = useSiteUrl();
const api = createActivityPubAPI(handle, siteUrl);
return useQuery({
queryKey: [`activities:${handle}`],
queryKey: [`activities:${handle}:includeOwn=${includeOwn.toString()}`],
async queryFn() {
return api.getAllActivities();
return api.getAllActivities(includeOwn);
}
});
}