From 995edaa966d5294fee8f29a1342a9d16cf6a5965 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Sat, 30 Nov 2024 17:35:38 +0000 Subject: [PATCH] Refactored minor things in admin-x-activitypub (#21769) Just a bunch of minor refactorings in admin-x-activitypub: - Reorganised api methods - Reorganised api query hooks - Reorganised api tests - Removed redundant `getActor` api method - Colocate `Search` type with component - Moved `ViewProfileModal` to `modals` - Refactored profile - Remove redundant `useFollowersForUser` - Remove redundant `useBrowseInboxForUser` - Removed redundant acceptance tests --- apps/admin-x-activitypub/src/MainContent.tsx | 37 --- .../src/api/activitypub.test.ts | 132 +++++----- .../src/api/activitypub.ts | 204 ++++++++------- .../src/components/Activities.tsx | 9 +- .../src/components/Inbox.tsx | 2 +- .../src/components/Profile.tsx | 17 +- .../src/components/Search.tsx | 6 +- .../{global => modals}/ViewProfileModal.tsx | 94 +++---- .../src/hooks/useActivityPubQueries.ts | 236 +++++++++--------- .../test/acceptance/app.test.ts | 10 - .../test/acceptance/inbox.test.ts | 9 + .../test/acceptance/listIndex.test.ts | 52 ---- 12 files changed, 358 insertions(+), 450 deletions(-) rename apps/admin-x-activitypub/src/components/{global => modals}/ViewProfileModal.tsx (91%) delete mode 100644 apps/admin-x-activitypub/test/acceptance/app.test.ts create mode 100644 apps/admin-x-activitypub/test/acceptance/inbox.test.ts delete mode 100644 apps/admin-x-activitypub/test/acceptance/listIndex.test.ts diff --git a/apps/admin-x-activitypub/src/MainContent.tsx b/apps/admin-x-activitypub/src/MainContent.tsx index 225a9cc915..5ceb696372 100644 --- a/apps/admin-x-activitypub/src/MainContent.tsx +++ b/apps/admin-x-activitypub/src/MainContent.tsx @@ -2,45 +2,8 @@ import Activities from './components/Activities'; import Inbox from './components/Inbox'; import Profile from './components/Profile'; import Search from './components/Search'; -import {ActivityPubAPI} from './api/activitypub'; -import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; -import {useQuery} from '@tanstack/react-query'; import {useRouting} from '@tryghost/admin-x-framework/routing'; -export function useBrowseInboxForUser(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: [`inbox:${handle}`], - async queryFn() { - return api.getInbox(); - } - }); -} - -export function useFollowersForUser(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: [`followers:${handle}`], - async queryFn() { - return api.getFollowers(); - } - }); -} - const MainContent = () => { const {route} = useRouting(); const mainRoute = route.split('/')[0]; diff --git a/apps/admin-x-activitypub/src/api/activitypub.test.ts b/apps/admin-x-activitypub/src/api/activitypub.test.ts index 6461b358ea..12300e0ee7 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.test.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.test.ts @@ -807,6 +807,35 @@ describe('ActivityPubAPI', function () { }); }); + describe('note', function () { + test('It creates a note and returns it', async function () { + const fakeFetch = Fetch({ + [`https://activitypub.api/.ghost/activitypub/actions/note`]: { + async assert(_resource, init) { + expect(init?.method).toEqual('POST'); + expect(init?.body).toEqual('{"content":"Hello, world!"}'); + }, + response: JSONResponse({ + id: 'https://example.com/note/abc123' + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const result = await api.note('Hello, world!'); + + expect(result).toEqual({ + id: 'https://example.com/note/abc123' + }); + }); + }); + describe('search', function () { test('It returns the results of the search', async function () { const handle = '@foo@bar.baz'; @@ -883,6 +912,43 @@ describe('ActivityPubAPI', function () { }); }); + describe('getProfile', function () { + test('It returns a profile', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}`]: { + response: JSONResponse({ + handle, + name: 'Foo Bar' + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getProfile(handle); + const expected = { + handle, + name: 'Foo Bar' + }; + + expect(actual).toEqual(expected); + }); + }); + describe('getFollowersForProfile', function () { test('It returns an array of followers for a profile', async function () { const handle = '@foo@bar.baz'; @@ -1597,43 +1663,6 @@ describe('ActivityPubAPI', function () { }); }); - describe('getProfile', function () { - test('It returns a profile', async function () { - const handle = '@foo@bar.baz'; - - const fakeFetch = Fetch({ - 'https://auth.api/': { - response: JSONResponse({ - identities: [{ - token: 'fake-token' - }] - }) - }, - [`https://activitypub.api/.ghost/activitypub/profile/${handle}`]: { - response: JSONResponse({ - handle, - name: 'Foo Bar' - }) - } - }); - - const api = new ActivityPubAPI( - new URL('https://activitypub.api'), - new URL('https://auth.api'), - 'index', - fakeFetch - ); - - const actual = await api.getProfile(handle); - const expected = { - handle, - name: 'Foo Bar' - }; - - expect(actual).toEqual(expected); - }); - }); - describe('getThread', function () { test('It returns a thread', async function () { const activityId = 'https://example.com/thread/abc123'; @@ -1670,33 +1699,4 @@ describe('ActivityPubAPI', function () { expect(actual).toEqual(expected); }); }); - - describe('note', function () { - test('It creates a note and returns it', async function () { - const fakeFetch = Fetch({ - [`https://activitypub.api/.ghost/activitypub/actions/note`]: { - async assert(_resource, init) { - expect(init?.method).toEqual('POST'); - expect(init?.body).toEqual('{"content":"Hello, world!"}'); - }, - response: JSONResponse({ - id: 'https://example.com/note/abc123' - }) - } - }); - - const api = new ActivityPubAPI( - new URL('https://activitypub.api'), - new URL('https://auth.api'), - 'index', - fakeFetch - ); - - const result = await api.note('Hello, world!'); - - expect(result).toEqual({ - id: 'https://example.com/note/abc123' - }); - }); - }); }); diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index a4b324b8af..d7abb4ec50 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -1,5 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Actor = any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Activity = any; @@ -15,6 +16,12 @@ export interface SearchResults { profiles: Profile[]; } +export interface ActivityThread { + items: Activity[]; +} + +export type ActivityPubCollectionResponse = {data: T[], next: string | null}; + export interface GetFollowersForProfileResponse { followers: { actor: Actor; @@ -36,12 +43,6 @@ export interface GetPostsForProfileResponse { next: string | null; } -export type ActivityPubCollectionResponse = {data: T[], next: string | null}; - -export interface ActivityThread { - items: Activity[]; -} - export class ActivityPubAPI { constructor( private readonly apiUrl: URL, @@ -177,110 +178,12 @@ export class ActivityPubAPI { return this.getActivityPubCollectionCount(this.followersApiUrl); } - async getFollowersForProfile(handle: string, next?: string): Promise { - const url = new URL(`.ghost/activitypub/profile/${handle}/followers`, this.apiUrl); - if (next) { - url.searchParams.set('next', next); - } - - const json = await this.fetchJSON(url); - - if (json === null) { - return { - followers: [], - next: null - }; - } - - if (!('followers' in json)) { - return { - followers: [], - next: null - }; - } - - const followers = Array.isArray(json.followers) ? json.followers : []; - const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; - - return { - followers, - next: nextPage - }; - } - - async getFollowingForProfile(handle: string, next?: string): Promise { - const url = new URL(`.ghost/activitypub/profile/${handle}/following`, this.apiUrl); - if (next) { - url.searchParams.set('next', next); - } - - const json = await this.fetchJSON(url); - - if (json === null) { - return { - following: [], - next: null - }; - } - - if (!('following' in json)) { - return { - following: [], - next: null - }; - } - - const following = Array.isArray(json.following) ? json.following : []; - const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; - - return { - following, - next: nextPage - }; - } - - async getPostsForProfile(handle: string, next?: string): Promise { - const url = new URL(`.ghost/activitypub/profile/${handle}/posts`, this.apiUrl); - if (next) { - url.searchParams.set('next', next); - } - - const json = await this.fetchJSON(url); - - if (json === null) { - return { - posts: [], - next: null - }; - } - - if (!('posts' in json)) { - return { - posts: [], - next: null - }; - } - - const posts = Array.isArray(json.posts) ? json.posts : []; - const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; - - return { - posts, - next: nextPage - }; - } - async follow(username: string): Promise { const url = new URL(`.ghost/activitypub/actions/follow/${username}`, this.apiUrl); const json = await this.fetchJSON(url, 'POST'); return json as Actor; } - async getActor(url: string): Promise { - const json = await this.fetchJSON(new URL(url)); - return json as Actor; - } - get likedApiUrl() { return new URL(`.ghost/activitypub/liked/${this.handle}`, this.apiUrl); } @@ -406,6 +309,99 @@ export class ActivityPubAPI { return json as Profile; } + async getFollowersForProfile(handle: string, next?: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}/followers`, this.apiUrl); + if (next) { + url.searchParams.set('next', next); + } + + const json = await this.fetchJSON(url); + + if (json === null) { + return { + followers: [], + next: null + }; + } + + if (!('followers' in json)) { + return { + followers: [], + next: null + }; + } + + const followers = Array.isArray(json.followers) ? json.followers : []; + const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; + + return { + followers, + next: nextPage + }; + } + + async getFollowingForProfile(handle: string, next?: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}/following`, this.apiUrl); + if (next) { + url.searchParams.set('next', next); + } + + const json = await this.fetchJSON(url); + + if (json === null) { + return { + following: [], + next: null + }; + } + + if (!('following' in json)) { + return { + following: [], + next: null + }; + } + + const following = Array.isArray(json.following) ? json.following : []; + const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; + + return { + following, + next: nextPage + }; + } + + async getPostsForProfile(handle: string, next?: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}/posts`, this.apiUrl); + if (next) { + url.searchParams.set('next', next); + } + + const json = await this.fetchJSON(url); + + if (json === null) { + return { + posts: [], + next: null + }; + } + + if (!('posts' in json)) { + return { + posts: [], + next: null + }; + } + + const posts = Array.isArray(json.posts) ? json.posts : []; + const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; + + return { + posts, + next: nextPage + }; + } + async getThread(id: string): Promise { const url = new URL(`.ghost/activitypub/thread/${encodeURIComponent(id)}`, this.apiUrl); const json = await this.fetchJSON(url); diff --git a/apps/admin-x-activitypub/src/components/Activities.tsx b/apps/admin-x-activitypub/src/components/Activities.tsx index c68e4d90dc..bb79ac86fa 100644 --- a/apps/admin-x-activitypub/src/components/Activities.tsx +++ b/apps/admin-x-activitypub/src/components/Activities.tsx @@ -9,7 +9,7 @@ import ActivityItem, {type Activity} from './activities/ActivityItem'; import ArticleModal from './feed/ArticleModal'; import MainNavigation from './navigation/MainNavigation'; import Separator from './global/Separator'; -import ViewProfileModal from './global/ViewProfileModal'; +import ViewProfileModal from './modals/ViewProfileModal'; import getUsername from '../utils/get-username'; import stripHtml from '../utils/strip-html'; @@ -127,13 +127,6 @@ const Activities: React.FC = ({}) => { }; }, [hasNextPage, isFetchingNextPage, fetchNextPage]); - // Retrieve followers for the user - // const {data: followers = []} = useFollowersForUser(user); - - // const isFollower = (id: string): boolean => { - // return followers.includes(id); - // }; - const handleActivityClick = (activity: Activity) => { switch (activity.type) { case ACTVITY_TYPE.CREATE: diff --git a/apps/admin-x-activitypub/src/components/Inbox.tsx b/apps/admin-x-activitypub/src/components/Inbox.tsx index 4806a8cd93..2a076ad6b0 100644 --- a/apps/admin-x-activitypub/src/components/Inbox.tsx +++ b/apps/admin-x-activitypub/src/components/Inbox.tsx @@ -7,7 +7,7 @@ import NewPostModal from './modals/NewPostModal'; import NiceModal from '@ebay/nice-modal-react'; import React, {useEffect, useRef} from 'react'; import Separator from './global/Separator'; -import ViewProfileModal from './global/ViewProfileModal'; +import ViewProfileModal from './modals/ViewProfileModal'; import getName from '../utils/get-name'; import getUsername from '../utils/get-username'; import useSuggestedProfiles from '../hooks/useSuggestedProfiles'; diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index a77bc19eb5..f91cd0e532 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -24,7 +24,7 @@ import ActivityItem from './activities/ActivityItem'; import FeedItem from './feed/FeedItem'; import MainNavigation from './navigation/MainNavigation'; import Separator from './global/Separator'; -import ViewProfileModal from './global/ViewProfileModal'; +import ViewProfileModal from './modals/ViewProfileModal'; import {type Activity} from '../components/activities/ActivityItem'; interface UseInfiniteScrollTabProps { @@ -83,11 +83,13 @@ const useInfiniteScrollTab = ({useDataHook, emptyStateLabel, emptyStateI const LoadingState = () => ( <>
- {(isLoading || isFetchingNextPage) && ( -
- -
- )} + { + (isLoading || isFetchingNextPage) && ( +
+ +
+ ) + } ); @@ -272,8 +274,7 @@ const Profile: React.FC = ({}) => {
- ), - counter: null + ) }, { id: 'likes', diff --git a/apps/admin-x-activitypub/src/components/Search.tsx b/apps/admin-x-activitypub/src/components/Search.tsx index d235938205..0df4643fda 100644 --- a/apps/admin-x-activitypub/src/components/Search.tsx +++ b/apps/admin-x-activitypub/src/components/Search.tsx @@ -10,7 +10,7 @@ import FollowButton from './global/FollowButton'; import MainNavigation from './navigation/MainNavigation'; import NiceModal from '@ebay/nice-modal-react'; -import ViewProfileModal from './global/ViewProfileModal'; +import ViewProfileModal from './modals/ViewProfileModal'; import Separator from './global/Separator'; import useSuggestedProfiles from '../hooks/useSuggestedProfiles'; @@ -29,8 +29,6 @@ interface SearchResultProps { update: (id: string, updated: Partial) => void; } -interface SearchProps {} - const SearchResult: React.FC = ({result, update}) => { const onFollow = () => { update(result.actor.id!, { @@ -120,6 +118,8 @@ const SuggestedAccounts: React.FC<{ ); }; +interface SearchProps {} + const Search: React.FC = ({}) => { // Initialise suggested profiles const {suggested, isLoadingSuggested, updateSuggestedProfile} = useSuggestedProfiles(6); diff --git a/apps/admin-x-activitypub/src/components/global/ViewProfileModal.tsx b/apps/admin-x-activitypub/src/components/modals/ViewProfileModal.tsx similarity index 91% rename from apps/admin-x-activitypub/src/components/global/ViewProfileModal.tsx rename to apps/admin-x-activitypub/src/components/modals/ViewProfileModal.tsx index 101339c0e7..6f4a35b1a6 100644 --- a/apps/admin-x-activitypub/src/components/global/ViewProfileModal.tsx +++ b/apps/admin-x-activitypub/src/components/modals/ViewProfileModal.tsx @@ -13,7 +13,7 @@ import APAvatar from '../global/APAvatar'; import ActivityItem from '../activities/ActivityItem'; import FeedItem from '../feed/FeedItem'; import FollowButton from '../global/FollowButton'; -import Separator from './Separator'; +import Separator from '../global/Separator'; import getName from '../../utils/get-name'; import getUsername from '../../utils/get-username'; @@ -21,7 +21,7 @@ const noop = () => {}; type QueryPageData = GetFollowersForProfileResponse | GetFollowingForProfileResponse; -type QueryFn = (handle: string) => UseInfiniteQueryResult; +type QueryFn = (handle: string) => UseInfiniteQueryResult; type ActorListProps = { handle: string, @@ -44,7 +44,7 @@ const ActorList: React.FC = ({ isLoading } = queryFn(handle); - const actorData = (data?.pages.flatMap(resolveDataFn) ?? []); + const actors = (data?.pages.flatMap(resolveDataFn) ?? []); const observerRef = useRef(null); const loadMoreRef = useRef(null); @@ -74,13 +74,13 @@ const ActorList: React.FC = ({ return (
{ - actorData.length === 0 && !isLoading ? ( + hasNextPage === false && actors.length === 0 ? ( {noResultsMessage} ) : ( - {actorData.map(({actor, isFollowing}, index) => { + {actors.map(({actor, isFollowing}, index) => { return ( @@ -98,7 +98,7 @@ const ActorList: React.FC = ({ type='link' /> - {index < actorData.length - 1 && } + {index < actors.length - 1 && } ); })} @@ -117,28 +117,6 @@ const ActorList: React.FC = ({ ); }; -const FollowersTab: React.FC<{handle: string}> = ({handle}) => { - return ( - ('followers' in page ? page.followers : [])} - /> - ); -}; - -const FollowingTab: React.FC<{handle: string}> = ({handle}) => { - return ( - ('following' in page ? page.following : [])} - /> - ); -}; - const PostsTab: React.FC<{handle: string}> = ({handle}) => { const { data, @@ -178,21 +156,29 @@ const PostsTab: React.FC<{handle: string}> = ({handle}) => { return (
- {posts.map((post, index) => ( -
- {}} - /> - {index < posts.length - 1 && ( - - )} -
- ))} + { + hasNextPage === false && posts.length === 0 ? ( + + {handle} has not posted anything yet + + ) : ( + <> + {posts.map((post, index) => ( +
+ {}} + /> + {index < posts.length - 1 && } +
+ ))} + + ) + }
{ (isFetchingNextPage || isLoading) && ( @@ -205,6 +191,28 @@ const PostsTab: React.FC<{handle: string}> = ({handle}) => { ); }; +const FollowingTab: React.FC<{handle: string}> = ({handle}) => { + return ( + ('following' in page ? page.following : [])} + /> + ); +}; + +const FollowersTab: React.FC<{handle: string}> = ({handle}) => { + return ( + ('followers' in page ? page.followers : [])} + /> + ); +}; + interface ViewProfileModalProps { profile: { actor: ActorProperties; diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 1129b06d69..e990f6576a 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -24,6 +24,20 @@ function createActivityPubAPI(handle: string, siteUrl: string) { ); } +export function useOutboxForUser(handle: string) { + return useInfiniteQuery({ + queryKey: [`outbox:${handle}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getOutbox(pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.next; + } + }); +} + export function useLikedForUser(handle: string) { return useInfiniteQuery({ queryKey: [`liked:${handle}`], @@ -38,12 +52,13 @@ export function useLikedForUser(handle: string) { }); } -export function useReplyMutationForUser(handle: string) { - return useMutation({ - async mutationFn({id, content}: {id: string, content: string}) { +export function useLikedCountForUser(handle: string) { + return useQuery({ + queryKey: [`likedCount:${handle}`], + async queryFn() { const siteUrl = await getSiteUrl(); const api = createActivityPubAPI(handle, siteUrl); - return await api.reply(id, content) as Activity; + return api.getLikedCount(); } }); } @@ -154,6 +169,20 @@ export function useUserDataForUser(handle: string) { }); } +export function useFollowersForUser(handle: string) { + return useInfiniteQuery({ + queryKey: [`followers:${handle}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getFollowers(pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.next; + } + }); +} + export function useFollowersCountForUser(handle: string) { return useQuery({ queryKey: [`followersCount:${handle}`], @@ -165,28 +194,6 @@ export function useFollowersCountForUser(handle: string) { }); } -export function useFollowingCountForUser(handle: string) { - return useQuery({ - queryKey: [`followingCount:${handle}`], - async queryFn() { - const siteUrl = await getSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); - return api.getFollowingCount(); - } - }); -} - -export function useLikedCountForUser(handle: string) { - return useQuery({ - queryKey: [`likedCount:${handle}`], - async queryFn() { - const siteUrl = await getSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); - return api.getLikedCount(); - } - }); -} - export function useFollowingForUser(handle: string) { return useInfiniteQuery({ queryKey: [`following:${handle}`], @@ -201,20 +208,56 @@ export function useFollowingForUser(handle: string) { }); } -export function useFollowersForUser(handle: string) { - return useInfiniteQuery({ - queryKey: [`followers:${handle}`], - async queryFn({pageParam}: {pageParam?: string}) { +export function useFollowingCountForUser(handle: string) { + return useQuery({ + queryKey: [`followingCount:${handle}`], + async queryFn() { const siteUrl = await getSiteUrl(); const api = createActivityPubAPI(handle, siteUrl); - return api.getFollowers(pageParam); - }, - getNextPageParam(prevPage) { - return prevPage.next; + return api.getFollowingCount(); } }); } +export function useFollow(handle: string, onSuccess: () => void, onError: () => void) { + const queryClient = useQueryClient(); + return useMutation({ + async mutationFn(username: string) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.follow(username); + }, + onSuccess(followedActor, fullHandle) { + queryClient.setQueryData([`profile:${fullHandle}`], (currentProfile: unknown) => { + if (!currentProfile) { + return currentProfile; + } + return { + ...currentProfile, + isFollowing: true + }; + }); + + queryClient.setQueryData(['following:index'], (currentFollowing?: unknown[]) => { + if (!currentFollowing) { + return currentFollowing; + } + return [followedActor].concat(currentFollowing); + }); + + queryClient.setQueryData(['followingCount:index'], (currentFollowingCount?: number) => { + if (!currentFollowingCount) { + return 1; + } + return currentFollowingCount + 1; + }); + + onSuccess(); + }, + onError + }); +} + export function useActivitiesForUser({ handle, includeOwn = false, @@ -303,87 +346,6 @@ export function useSearchForUser(handle: string, query: string) { return {searchQuery, updateProfileSearchResult}; } -export function useFollow(handle: string, onSuccess: () => void, onError: () => void) { - const queryClient = useQueryClient(); - return useMutation({ - async mutationFn(username: string) { - const siteUrl = await getSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); - return api.follow(username); - }, - onSuccess(followedActor, fullHandle) { - queryClient.setQueryData([`profile:${fullHandle}`], (currentProfile: unknown) => { - if (!currentProfile) { - return currentProfile; - } - return { - ...currentProfile, - isFollowing: true - }; - }); - - queryClient.setQueryData(['following:index'], (currentFollowing?: unknown[]) => { - if (!currentFollowing) { - return currentFollowing; - } - return [followedActor].concat(currentFollowing); - }); - - queryClient.setQueryData(['followingCount:index'], (currentFollowingCount?: number) => { - if (!currentFollowingCount) { - return 1; - } - return currentFollowingCount + 1; - }); - - onSuccess(); - }, - onError - }); -} - -export function useFollowersForProfile(handle: string) { - return useInfiniteQuery({ - queryKey: [`followers:${handle}`], - async queryFn({pageParam}: {pageParam?: string}) { - const siteUrl = await getSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); - return api.getFollowersForProfile(handle, pageParam); - }, - getNextPageParam(prevPage) { - return prevPage.next; - } - }); -} - -export function useFollowingForProfile(handle: string) { - return useInfiniteQuery({ - queryKey: [`following:${handle}`], - async queryFn({pageParam}: {pageParam?: string}) { - const siteUrl = await getSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); - return api.getFollowingForProfile(handle, pageParam); - }, - getNextPageParam(prevPage) { - return prevPage.next; - } - }); -} - -export function usePostsForProfile(handle: string) { - return useInfiniteQuery({ - queryKey: [`posts:${handle}`], - async queryFn({pageParam}: {pageParam?: string}) { - const siteUrl = await getSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); - return api.getPostsForProfile(handle, pageParam); - }, - getNextPageParam(prevPage) { - return prevPage.next; - } - }); -} - export function useSuggestedProfiles(handle: string, handles: string[]) { const queryClient = useQueryClient(); const queryKey = ['profiles', {handles}]; @@ -434,13 +396,41 @@ export function useProfileForUser(handle: string, fullHandle: string, enabled: b }); } -export function useOutboxForUser(handle: string) { +export function usePostsForProfile(handle: string) { return useInfiniteQuery({ - queryKey: [`outbox:${handle}`], + queryKey: [`posts:${handle}`], async queryFn({pageParam}: {pageParam?: string}) { const siteUrl = await getSiteUrl(); const api = createActivityPubAPI(handle, siteUrl); - return api.getOutbox(pageParam); + return api.getPostsForProfile(handle, pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.next; + } + }); +} + +export function useFollowersForProfile(handle: string) { + return useInfiniteQuery({ + queryKey: [`followers:${handle}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getFollowersForProfile(handle, pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.next; + } + }); +} + +export function useFollowingForProfile(handle: string) { + return useInfiniteQuery({ + queryKey: [`following:${handle}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getFollowingForProfile(handle, pageParam); }, getNextPageParam(prevPage) { return prevPage.next; @@ -476,6 +466,16 @@ export function useThreadForUser(handle: string, id: string) { return {threadQuery, addToThread}; } +export function useReplyMutationForUser(handle: string) { + return useMutation({ + async mutationFn({id, content}: {id: string, content: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return await api.reply(id, content) as Activity; + } + }); +} + export function useNoteMutationForUser(handle: string) { const queryClient = useQueryClient(); diff --git a/apps/admin-x-activitypub/test/acceptance/app.test.ts b/apps/admin-x-activitypub/test/acceptance/app.test.ts deleted file mode 100644 index 90fa60d842..0000000000 --- a/apps/admin-x-activitypub/test/acceptance/app.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {expect, test} from '@playwright/test'; -// import {mockApi, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance'; - -test.describe('Demo', async () => { - test('Renders the list page', async ({page}) => { - await page.goto('/'); - - await expect(page.locator('body')).toContainText('ActivityPub Inbox'); - }); -}); diff --git a/apps/admin-x-activitypub/test/acceptance/inbox.test.ts b/apps/admin-x-activitypub/test/acceptance/inbox.test.ts new file mode 100644 index 0000000000..e045acfb5a --- /dev/null +++ b/apps/admin-x-activitypub/test/acceptance/inbox.test.ts @@ -0,0 +1,9 @@ +import {expect, test} from '@playwright/test'; + +test.describe('Inbox', async () => { + test('Renders the inbox page', async ({page}) => { + await page.goto('/'); + + await expect(page.locator('body')).toContainText('This is your inbox'); + }); +}); diff --git a/apps/admin-x-activitypub/test/acceptance/listIndex.test.ts b/apps/admin-x-activitypub/test/acceptance/listIndex.test.ts deleted file mode 100644 index 8e855d1e21..0000000000 --- a/apps/admin-x-activitypub/test/acceptance/listIndex.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {expect, test} from '@playwright/test'; -import {mockApi, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance'; - -test.describe('ListIndex', async () => { - test('Renders the list page', async ({page}) => { - const userId = 'index'; - await mockApi({ - page, - requests: { - useBrowseInboxForUser: {method: 'GET', path: `/inbox/${userId}`, response: responseFixtures.activitypubInbox}, - useBrowseFollowingForUser: {method: 'GET', path: `/following/${userId}`, response: responseFixtures.activitypubFollowing} - }, - options: {useActivityPub: true} - }); - - // Printing browser consol logs - page.on('console', (msg) => { - console.log(`Browser console log: ${msg.type()}: ${msg.text()}`); /* eslint-disable-line no-console */ - }); - - await page.goto('/'); - - await expect(page.locator('body')).toContainText('ActivityPub Inbox'); - - // following list - const followingUser = await page.locator('[data-test-following] > li').textContent(); - await expect(followingUser).toEqual('@index@main.ghost.org'); - const followingCount = await page.locator('[data-test-following-count]').textContent(); - await expect(followingCount).toEqual('1'); - - // following button - const followingList = await page.locator('[data-test-following-modal]'); - await expect(followingList).toBeVisible(); - - // activities - const activity = await page.locator('[data-test-activity-heading]').textContent(); - await expect(activity).toEqual('Testing ActivityPub'); - - // click on article - const articleBtn = await page.locator('[data-test-view-article]'); - await articleBtn.click(); - - // article is expanded - const frameLocator = page.frameLocator('#gh-ap-article-iframe'); - const textElement = await frameLocator.locator('[data-test-article-heading]').innerText(); - expect(textElement).toContain('Testing ActivityPub'); - - // go back to list - const backBtn = await page.locator('[data-test-back-button]'); - await backBtn.click(); - }); -});