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:
parent
c7d4facd85
commit
995edaa966
12 changed files with 358 additions and 450 deletions
|
@ -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];
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
9
apps/admin-x-activitypub/test/acceptance/inbox.test.ts
Normal file
9
apps/admin-x-activitypub/test/acceptance/inbox.test.ts
Normal 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');
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue