From bfd3ee1209c0c5b13957131428062ffc93aa02f2 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Mon, 22 Jul 2024 18:18:45 +0700 Subject: [PATCH] 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. --- .../src/components/FollowSite.tsx | 81 ++++++++----------- .../src/components/ListIndex.tsx | 62 ++++++++++++-- .../src/components/ViewFollowers.tsx | 47 ++++++++--- .../src/components/ViewFollowing.tsx | 31 ++++--- 4 files changed, 148 insertions(+), 73 deletions(-) diff --git a/apps/admin-x-activitypub/src/components/FollowSite.tsx b/apps/admin-x-activitypub/src/components/FollowSite.tsx index c76fb6c64a..55f613d18a 100644 --- a/apps/admin-x-activitypub/src/components/FollowSite.tsx +++ b/apps/admin-x-activitypub/src/components/FollowSite.tsx @@ -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 ( { okLabel='Follow' size='sm' title='Follow a Ghost site' - onOk={handleFollow} + onOk={() => mutation.mutate(profileName)} >
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(null); const [, setArticleActor] = useState(null); diff --git a/apps/admin-x-activitypub/src/components/ViewFollowers.tsx b/apps/admin-x-activitypub/src/components/ViewFollowers.tsx index bff12ef78c..99cc96fa17 100644 --- a/apps/admin-x-activitypub/src/components/ViewFollowers.tsx +++ b/apps/admin-x-activitypub/src/components/ViewFollowers.tsx @@ -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 = ({}) => { +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 = ({}) => { 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
{followers.map(item => ( - mutation.mutate({username: getUsername(item)})} />} avatar={} detail={getUsername(item)} id='list-item' title={item.name}> + mutation.mutate(getUsername(item))} />} avatar={} detail={getUsername(item)} id='list-item' title={item.name}> ))}
diff --git a/apps/admin-x-activitypub/src/components/ViewFollowing.tsx b/apps/admin-x-activitypub/src/components/ViewFollowing.tsx index 4a5bc82cd6..4fc3de36c4 100644 --- a/apps/admin-x-activitypub/src/components/ViewFollowing.tsx +++ b/apps/admin-x-activitypub/src/components/ViewFollowing.tsx @@ -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 = ({}) => { +const ViewFollowingModal: React.FC = ({}) => { const {updateRoute} = useRouting(); - const mutation = useUnfollow(); - const {data: {items = []} = {}} = useBrowseFollowingForUser('inbox'); + const {data: items = []} = useFollowingForUser('index'); const following = Array.isArray(items) ? items : [items]; return ( { - mutation.reset(); updateRoute(''); }} cancelLabel='' @@ -33,7 +44,7 @@ const ViewFollowingModal: React.FC
{following.map(item => ( - mutation.mutate({username: getUsername(item)})} />} avatar={} detail={getUsername(item)} id='list-item' title={item.name}> + } avatar={} detail={getUsername(item)} id='list-item' title={item.name}> ))} {/*