diff --git a/apps/admin-x-activitypub/src/api/activitypub.test.ts b/apps/admin-x-activitypub/src/api/activitypub.test.ts index 85f70b71ed94..88b7dab110be 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.test.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.test.ts @@ -1091,4 +1091,41 @@ describe('ActivityPubAPI', function () { expect(actual.following).toEqual([]); }); }); + + 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); + }); + }); }); diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index 78f6ff945703..977c3ef75a13 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -3,7 +3,7 @@ export type Actor = any; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Activity = any; -export interface ProfileSearchResult { +export interface Profile { actor: Actor; handle: string; followerCount: number; @@ -12,7 +12,7 @@ export interface ProfileSearchResult { } export interface SearchResults { - profiles: ProfileSearchResult[]; + profiles: Profile[]; } export interface GetFollowersForProfileResponse { @@ -390,4 +390,10 @@ export class ActivityPubAPI { profiles: [] }; } + + async getProfile(handle: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}`, this.apiUrl); + const json = await this.fetchJSON(url); + return json as Profile; + } } diff --git a/apps/admin-x-activitypub/src/components/Search.tsx b/apps/admin-x-activitypub/src/components/Search.tsx index aa8cef13598a..94bb0ced4a8f 100644 --- a/apps/admin-x-activitypub/src/components/Search.tsx +++ b/apps/admin-x-activitypub/src/components/Search.tsx @@ -12,7 +12,7 @@ import MainNavigation from './navigation/MainNavigation'; import NiceModal from '@ebay/nice-modal-react'; import ProfileSearchResultModal from './search/ProfileSearchResultModal'; -import {useSearchForUser} from '../hooks/useActivityPubQueries'; +import {useSearchForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries'; interface SearchResultItem { actor: ActorProperties; @@ -73,72 +73,8 @@ const SearchResult: React.FC = ({result, update}) => { const Search: React.FC = ({}) => { // Initialise suggested profiles - const [suggested, setSuggested] = useState([ - { - actor: { - id: 'https://mastodon.social/@quillmatiq', - name: 'Anuj Ahooja', - preferredUsername: '@quillmatiq@mastodon.social', - image: { - url: 'https://anujahooja.com/assets/images/image12.jpg?v=601ebe30' - }, - icon: { - url: 'https://anujahooja.com/assets/images/image12.jpg?v=601ebe30' - } - } as ActorProperties, - handle: '@quillmatiq@mastodon.social', - followerCount: 436, - followingCount: 634, - isFollowing: false, - posts: [] - }, - { - actor: { - id: 'https://flipboard.social/@miaq', - name: 'Mia Quagliarello', - preferredUsername: '@miaq@flipboard.social', - image: { - url: 'https://m-cdn.flipboard.social/accounts/avatars/109/824/428/955/351/328/original/383f288b81ab280c.png' - }, - icon: { - url: 'https://m-cdn.flipboard.social/accounts/avatars/109/824/428/955/351/328/original/383f288b81ab280c.png' - } - } as ActorProperties, - handle: '@miaq@flipboard.social', - followerCount: 533, - followingCount: 335, - isFollowing: false, - posts: [] - }, - { - actor: { - id: 'https://techpolicy.social/@mallory', - name: 'Mallory', - preferredUsername: '@mallory@techpolicy.social', - image: { - url: 'https://techpolicy.social/system/accounts/avatars/109/378/338/180/403/396/original/20b043b0265cac73.jpeg' - }, - icon: { - url: 'https://techpolicy.social/system/accounts/avatars/109/378/338/180/403/396/original/20b043b0265cac73.jpeg' - } - } as ActorProperties, - handle: '@mallory@techpolicy.social', - followerCount: 1100, - followingCount: 11, - isFollowing: false, - posts: [] - } - ]); - - const updateSuggested = (id: string, updated: Partial) => { - const index = suggested.findIndex(result => result.actor.id === id); - - setSuggested((current) => { - const newSuggested = [...current]; - newSuggested[index] = {...newSuggested[index], ...updated}; - return newSuggested; - }); - }; + const {suggestedProfilesQuery, updateSuggestedProfile} = useSuggestedProfiles('index', ['@quillmatiq@mastodon.social', '@miaq@flipboard.social', '@mallory@techpolicy.social']); + const {data: suggested = [], isLoading: isLoadingSuggested} = suggestedProfilesQuery; // Initialise search query const queryInputRef = useRef(null); @@ -220,11 +156,14 @@ const Search: React.FC = ({}) => { {showSuggested && ( <> Suggested accounts + {isLoadingSuggested && ( + + )} {suggested.map(profile => ( ))} diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 8d3ea624a16d..0e1ecc824f40 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -1,5 +1,5 @@ import {Activity} from '../components/activities/ActivityItem'; -import {ActivityPubAPI, type ProfileSearchResult, type SearchResults} from '../api/activitypub'; +import {ActivityPubAPI, type Profile, type SearchResults} from '../api/activitypub'; import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; import {useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; @@ -246,7 +246,7 @@ export function useSearchForUser(handle: string, query: string) { } }); - const updateProfileSearchResult = (id: string, updated: Partial) => { + const updateProfileSearchResult = (id: string, updated: Partial) => { queryClient.setQueryData(queryKey, (current: SearchResults | undefined) => { if (!current) { return current; @@ -254,7 +254,7 @@ export function useSearchForUser(handle: string, query: string) { return { ...current, - profiles: current.profiles.map((item: ProfileSearchResult) => { + profiles: current.profiles.map((item: Profile) => { if (item.actor.id === id) { return {...item, ...updated}; } @@ -306,3 +306,47 @@ export function useFollowingForProfile(handle: string) { } }); } + +export function useSuggestedProfiles(handle: string, handles: string[]) { + const siteUrl = useSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + const queryClient = useQueryClient(); + const queryKey = ['profiles', {handles}]; + + const suggestedProfilesQuery = useQuery({ + queryKey, + async queryFn() { + return Promise.all( + handles.map(h => api.getProfile(h)) + ); + } + }); + + const updateSuggestedProfile = (id: string, updated: Partial) => { + queryClient.setQueryData(queryKey, (current: Profile[] | undefined) => { + if (!current) { + return current; + } + + return current.map((item: Profile) => { + if (item.actor.id === id) { + return {...item, ...updated}; + } + return item; + }); + }); + }; + + return {suggestedProfilesQuery, updateSuggestedProfile}; +} + +export function useProfileForUser(handle: string, fullHandle: string) { + const siteUrl = useSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return useQuery({ + queryKey: [`profile:${fullHandle}`], + async queryFn() { + return api.getProfile(fullHandle); + } + }); +}