Wired up ActivityPubAPI w/ ReactQuery

This is a bit of a stopgap, we'll want to eventually pull these hooks out into
a shared file, but for now this is fine.

This almost decouples us from admin-x-framework, but we're still using it to
get the site url as well as some types that we can pull out later.
This commit is contained in:
Fabien O'Carroll 2024-07-22 18:18:45 +07:00 committed by Fabien 'egg' O'Carroll
parent c6e407fb7e
commit bfd3ee1209
4 changed files with 148 additions and 73 deletions

View File

@ -1,65 +1,48 @@
import NiceModal from '@ebay/nice-modal-react';
import {ActivityPubAPI} from '../api/activitypub';
import {Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useFollow} from '@tryghost/admin-x-framework/api/activitypub';
import {useQueryClient} from '@tryghost/admin-x-framework';
import {useMutation} from '@tanstack/react-query';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useState} from 'react';
// const sleep = (ms: number) => (
// new Promise((resolve) => {
// setTimeout(resolve, ms);
// })
// );
function useFollow(handle: string, onSuccess: () => void, onError: () => void) {
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 useMutation({
async mutationFn(username: string) {
return api.follow(username);
},
onSuccess,
onError
});
}
const FollowSite = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const mutation = useFollow();
const client = useQueryClient();
const site = useBrowseSite();
const siteData = site.data?.site;
const siteUrl = siteData?.url ?? window.location.origin;
// mutation.isPending
// mutation.isError
// mutation.isSuccess
// mutation.mutate({username: '@index@site.com'})
// mutation.reset();
// State to manage the text field value
const [profileName, setProfileName] = useState('');
// const [success, setSuccess] = useState(false);
const [errorMessage, setError] = useState(null);
const handleFollow = async () => {
try {
const url = new URL(`.ghost/activitypub/actions/follow/${profileName}`, siteUrl);
await fetch(url, {
method: 'POST'
});
// Perform the mutation
// If successful, set the success state to true
// setSuccess(true);
showToast({
message: 'Site followed',
type: 'success'
});
async function onSuccess() {
showToast({
message: 'Site followed',
type: 'success'
});
// // Because we don't return the new follower data from the API, we need to wait a bit to let it process and then update the query.
// // This is a dirty hack and should be replaced with a better solution.
// await sleep(2000);
modal.remove();
// Refetch the following data.
// At this point it might not be updated yet, but it will be eventually.
await client.refetchQueries({queryKey: ['FollowingResponseData'], type: 'active'});
updateRoute('');
} catch (error) {
// If there's an error, set the error state
setError(errorMessage);
}
};
modal.remove();
updateRoute('');
}
async function onError() {
setError(errorMessage);
}
const mutation = useFollow('index', onSuccess, onError);
return (
<Modal
@ -71,7 +54,7 @@ const FollowSite = NiceModal.create(() => {
okLabel='Follow'
size='sm'
title='Follow a Ghost site'
onOk={handleFollow}
onOk={() => mutation.mutate(profileName)}
>
<div className='mt-3 flex flex-col gap-4'>
<TextField

View File

@ -1,10 +1,11 @@
// import NiceModal from '@ebay/nice-modal-react';
import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
import React, {useEffect, useRef, useState} from 'react';
import articleBodyStyles from './articleBodyStyles';
import {ActorProperties, ObjectProperties, useBrowseFollowersForUser, useBrowseFollowingForUser, useBrowseInboxForUser} from '@tryghost/admin-x-framework/api/activitypub';
import {ActivityPubAPI} from '../api/activitypub';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Avatar, Button, ButtonGroup, Heading, List, ListItem, Page, SelectOption, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useQuery} from '@tanstack/react-query';
import {useRouting} from '@tryghost/admin-x-framework/routing';
interface ViewArticleProps {
@ -12,13 +13,64 @@ interface ViewArticleProps {
onBackToList: () => void;
}
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();
}
});
}
function useFollowersCountForUser(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: [`followersCount:${handle}`],
async queryFn() {
return api.getFollowersCount();
}
});
}
function useFollowingCountForUser(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: [`followingCount:${handle}`],
async queryFn() {
return api.getFollowingCount();
}
});
}
const ActivityPubComponent: React.FC = () => {
const {updateRoute} = useRouting();
// TODO: Replace with actual user ID
const {data: {items: activities = []} = {}} = useBrowseInboxForUser('index');
const {data: {totalItems: followingCount = 0} = {}} = useBrowseFollowingForUser('index');
const {data: {totalItems: followersCount = 0} = {}} = useBrowseFollowersForUser('index');
const {data: activities = []} = useBrowseInboxForUser('index');
const {data: followersCount = 0} = useFollowersCountForUser('index');
const {data: followingCount = 0} = useFollowingCountForUser('index');
const [articleContent, setArticleContent] = useState<ObjectProperties | null>(null);
const [, setArticleActor] = useState<ActorProperties | null>(null);

View File

@ -1,21 +1,50 @@
import {} from '@tryghost/admin-x-framework/api/activitypub';
import NiceModal from '@ebay/nice-modal-react';
import getUsername from '../utils/get-username';
import {ActivityPubAPI} from '../api/activitypub';
import {Avatar, Button, List, ListItem, Modal} from '@tryghost/admin-x-design-system';
import {FollowingResponseData, useBrowseFollowersForUser, useFollow} from '@tryghost/admin-x-framework/api/activitypub';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useMutation, useQuery} from '@tanstack/react-query';
interface ViewFollowersModalProps {
following: FollowingResponseData[],
animate?: boolean
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 ViewFollowersModal: React.FC<RoutingModalProps & ViewFollowersModalProps> = ({}) => {
function useFollow(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 useMutation({
async mutationFn(username: string) {
return api.follow(username);
}
});
}
const ViewFollowersModal: React.FC<RoutingModalProps> = ({}) => {
const {updateRoute} = useRouting();
// const modal = NiceModal.useModal();
const mutation = useFollow();
const mutation = useFollow('index');
const {data: {items = []} = {}} = useBrowseFollowersForUser('inbox');
const {data: items = []} = useFollowersForUser('index');
const followers = Array.isArray(items) ? items : [items];
return (
@ -34,7 +63,7 @@ const ViewFollowersModal: React.FC<RoutingModalProps & ViewFollowersModalProps>
<div className='mt-3 flex flex-col gap-4 pb-12'>
<List>
{followers.map(item => (
<ListItem action={<Button color='grey' label='Follow back' link={true} onClick={() => mutation.mutate({username: getUsername(item)})} />} avatar={<Avatar image={item.icon} size='sm' />} detail={getUsername(item)} id='list-item' title={item.name}></ListItem>
<ListItem action={<Button color='grey' label='Follow back' link={true} onClick={() => mutation.mutate(getUsername(item))} />} avatar={<Avatar image={item.icon} size='sm' />} detail={getUsername(item)} id='list-item' title={item.name}></ListItem>
))}
</List>
</div>

View File

@ -1,26 +1,37 @@
import {} from '@tryghost/admin-x-framework/api/activitypub';
import NiceModal from '@ebay/nice-modal-react';
import getUsername from '../utils/get-username';
import {ActivityPubAPI} from '../api/activitypub';
import {Avatar, Button, List, ListItem, Modal} from '@tryghost/admin-x-design-system';
import {FollowingResponseData, useBrowseFollowingForUser, useUnfollow} from '@tryghost/admin-x-framework/api/activitypub';
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useQuery} from '@tanstack/react-query';
interface ViewFollowingModalProps {
following: FollowingResponseData[],
animate?: boolean
function useFollowingForUser(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: [`following:${handle}`],
async queryFn() {
return api.getFollowing();
}
});
}
const ViewFollowingModal: React.FC<RoutingModalProps & ViewFollowingModalProps> = ({}) => {
const ViewFollowingModal: React.FC<RoutingModalProps> = ({}) => {
const {updateRoute} = useRouting();
const mutation = useUnfollow();
const {data: {items = []} = {}} = useBrowseFollowingForUser('inbox');
const {data: items = []} = useFollowingForUser('index');
const following = Array.isArray(items) ? items : [items];
return (
<Modal
afterClose={() => {
mutation.reset();
updateRoute('');
}}
cancelLabel=''
@ -33,7 +44,7 @@ const ViewFollowingModal: React.FC<RoutingModalProps & ViewFollowingModalProps>
<div className='mt-3 flex flex-col gap-4 pb-12'>
<List>
{following.map(item => (
<ListItem action={<Button color='grey' label='Unfollow' link={true} onClick={() => mutation.mutate({username: getUsername(item)})} />} avatar={<Avatar image={item.icon} size='sm' />} detail={getUsername(item)} id='list-item' title={item.name}></ListItem>
<ListItem action={<Button color='grey' label='Unfollow' link={true} />} avatar={<Avatar image={item.icon} size='sm' />} detail={getUsername(item)} id='list-item' title={item.name}></ListItem>
))}
</List>
{/* <Table>