0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

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
This commit is contained in:
Michael Barrett 2024-11-30 17:35:38 +00:00 committed by GitHub
parent c7d4facd85
commit 995edaa966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 358 additions and 450 deletions

View file

@ -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];

View file

@ -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'
});
});
});
});

View file

@ -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<T> = {data: T[], next: string | null};
export interface GetFollowersForProfileResponse {
followers: {
actor: Actor;
@ -36,12 +43,6 @@ export interface GetPostsForProfileResponse {
next: string | null;
}
export type ActivityPubCollectionResponse<T> = {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<GetFollowersForProfileResponse> {
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<GetFollowingForProfileResponse> {
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<GetPostsForProfileResponse> {
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<Actor> {
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<Actor> {
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<GetFollowersForProfileResponse> {
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<GetFollowingForProfileResponse> {
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<GetPostsForProfileResponse> {
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<ActivityThread> {
const url = new URL(`.ghost/activitypub/thread/${encodeURIComponent(id)}`, this.apiUrl);
const json = await this.fetchJSON(url);

View file

@ -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<ActivitiesProps> = ({}) => {
};
}, [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:

View file

@ -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';

View file

@ -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<TData> {
@ -83,11 +83,13 @@ const useInfiniteScrollTab = <TData,>({useDataHook, emptyStateLabel, emptyStateI
const LoadingState = () => (
<>
<div ref={loadMoreRef} className='h-1'></div>
{(isLoading || isFetchingNextPage) && (
<div className='mt-6 flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
</div>
)}
{
(isLoading || isFetchingNextPage) && (
<div className='mt-6 flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
</div>
)
}
</>
);
@ -272,8 +274,7 @@ const Profile: React.FC<ProfileProps> = ({}) => {
<div className='ap-posts'>
<PostsTab />
</div>
),
counter: null
)
},
{
id: 'likes',

View file

@ -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<SearchResultItem>) => void;
}
interface SearchProps {}
const SearchResult: React.FC<SearchResultProps> = ({result, update}) => {
const onFollow = () => {
update(result.actor.id!, {
@ -120,6 +118,8 @@ const SuggestedAccounts: React.FC<{
);
};
interface SearchProps {}
const Search: React.FC<SearchProps> = ({}) => {
// Initialise suggested profiles
const {suggested, isLoadingSuggested, updateSuggestedProfile} = useSuggestedProfiles(6);

View file

@ -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<QueryPageData, unknown>;
type QueryFn = (handle: string) => UseInfiniteQueryResult<QueryPageData>;
type ActorListProps = {
handle: string,
@ -44,7 +44,7 @@ const ActorList: React.FC<ActorListProps> = ({
isLoading
} = queryFn(handle);
const actorData = (data?.pages.flatMap(resolveDataFn) ?? []);
const actors = (data?.pages.flatMap(resolveDataFn) ?? []);
const observerRef = useRef<IntersectionObserver | null>(null);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
@ -74,13 +74,13 @@ const ActorList: React.FC<ActorListProps> = ({
return (
<div>
{
actorData.length === 0 && !isLoading ? (
hasNextPage === false && actors.length === 0 ? (
<NoValueLabel icon='user-add'>
{noResultsMessage}
</NoValueLabel>
) : (
<List>
{actorData.map(({actor, isFollowing}, index) => {
{actors.map(({actor, isFollowing}, index) => {
return (
<React.Fragment key={actor.id}>
<ActivityItem key={actor.id} url={actor.url}>
@ -98,7 +98,7 @@ const ActorList: React.FC<ActorListProps> = ({
type='link'
/>
</ActivityItem>
{index < actorData.length - 1 && <Separator />}
{index < actors.length - 1 && <Separator />}
</React.Fragment>
);
})}
@ -117,28 +117,6 @@ const ActorList: React.FC<ActorListProps> = ({
);
};
const FollowersTab: React.FC<{handle: string}> = ({handle}) => {
return (
<ActorList
handle={handle}
noResultsMessage={`${handle} has no followers yet`}
queryFn={useFollowersForProfile}
resolveDataFn={page => ('followers' in page ? page.followers : [])}
/>
);
};
const FollowingTab: React.FC<{handle: string}> = ({handle}) => {
return (
<ActorList
handle={handle}
noResultsMessage={`${handle} is not following anyone yet`}
queryFn={useFollowingForProfile}
resolveDataFn={page => ('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 (
<div>
{posts.map((post, index) => (
<div>
<FeedItem
actor={post.actor}
commentCount={post.object.replyCount}
layout='feed'
object={post.object}
type={post.type}
onCommentClick={() => {}}
/>
{index < posts.length - 1 && (
<Separator />
)}
</div>
))}
{
hasNextPage === false && posts.length === 0 ? (
<NoValueLabel icon='pen'>
{handle} has not posted anything yet
</NoValueLabel>
) : (
<>
{posts.map((post, index) => (
<div>
<FeedItem
actor={post.actor}
commentCount={post.object.replyCount}
layout='feed'
object={post.object}
type={post.type}
onCommentClick={() => {}}
/>
{index < posts.length - 1 && <Separator />}
</div>
))}
</>
)
}
<div ref={loadMoreRef} className='h-1'></div>
{
(isFetchingNextPage || isLoading) && (
@ -205,6 +191,28 @@ const PostsTab: React.FC<{handle: string}> = ({handle}) => {
);
};
const FollowingTab: React.FC<{handle: string}> = ({handle}) => {
return (
<ActorList
handle={handle}
noResultsMessage={`${handle} is not following anyone yet`}
queryFn={useFollowingForProfile}
resolveDataFn={page => ('following' in page ? page.following : [])}
/>
);
};
const FollowersTab: React.FC<{handle: string}> = ({handle}) => {
return (
<ActorList
handle={handle}
noResultsMessage={`${handle} has no followers yet`}
queryFn={useFollowersForProfile}
resolveDataFn={page => ('followers' in page ? page.followers : [])}
/>
);
};
interface ViewProfileModalProps {
profile: {
actor: ActorProperties;

View file

@ -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();

View file

@ -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');
});
});

View file

@ -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');
});
});

View file

@ -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();
});
});