diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index b287246df9..d9a0138571 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.3.49", + "version": "0.3.50", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-x-activitypub/src/api/activitypub.test.ts b/apps/admin-x-activitypub/src/api/activitypub.test.ts index 12300e0ee7..79c9b7ccdc 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.test.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.test.ts @@ -850,7 +850,7 @@ describe('ActivityPubAPI', function () { }, [`https://activitypub.api/.ghost/activitypub/actions/search?query=${encodeURIComponent(handle)}`]: { response: JSONResponse({ - profiles: [ + accounts: [ { handle, name: 'Foo Bar' @@ -869,7 +869,7 @@ describe('ActivityPubAPI', function () { const actual = await api.search(handle); const expected = { - profiles: [ + accounts: [ { handle, name: 'Foo Bar' @@ -880,7 +880,7 @@ describe('ActivityPubAPI', function () { expect(actual).toEqual(expected); }); - test('It returns an empty array when there are no profiles in the response', async function () { + test('It returns an empty array when there are no accounts in the response', async function () { const handle = '@foo@bar.baz'; const fakeFetch = Fetch({ @@ -905,7 +905,7 @@ describe('ActivityPubAPI', function () { const actual = await api.search(handle); const expected = { - profiles: [] + accounts: [] }; 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 a8922b7da6..d2fbed84da 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -12,8 +12,30 @@ export interface Profile { isFollowing: boolean; } +interface Account { + id: string; + name: string; + handle: string; + bio: string; + url: string; + avatarUrl: string; + bannerImageUrl: string | null; + customFields: Record; + postCount: number; + likedCount: number; + followingCount: number; + followerCount: number; + followsMe: boolean; + followedByMe: boolean; +} + +export type AccountSearchResult = Pick< + Account, + 'id' | 'name' | 'handle' | 'avatarUrl' | 'followedByMe' | 'followerCount' +>; + export interface SearchResults { - profiles: Profile[]; + accounts: AccountSearchResult[]; } export interface ActivityThread { @@ -45,29 +67,12 @@ export interface GetPostsForProfileResponse { export type AccountFollowsType = 'following' | 'followers'; -interface Account { - id: string; - name: string; - handle: string; - bio: string; - url: string; - avatarUrl: string; - bannerImageUrl: string | null; - customFields: Record; - postsCount: number; - likedCount: number; - followingCount: number; - followerCount: number; - followsMe: boolean; - followedByMe: boolean; -} - type GetAccountResponse = Account -export type MinimalAccount = Pick; +export type FollowAccount = Pick; export interface GetAccountFollowsResponse { - accounts: MinimalAccount[]; + accounts: FollowAccount[]; next: string | null; } @@ -293,12 +298,12 @@ export class ActivityPubAPI { const json = await this.fetchJSON(url, 'GET'); - if (json && 'profiles' in json) { + if (json && 'accounts' in json) { return json as SearchResults; } return { - profiles: [] + accounts: [] }; } diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index e45d8f5228..d9c50901e3 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -12,7 +12,7 @@ import { useLikedForUser, useOutboxForUser } from '../hooks/useActivityPubQueries'; -import {MinimalAccount} from '../api/activitypub'; +import {FollowAccount} from '../api/activitypub'; import {handleViewContent} from '../utils/content-handlers'; import APAvatar from './global/APAvatar'; @@ -181,7 +181,7 @@ const handleAccountClick = (handle: string) => { }; const FollowingTab: React.FC = () => { - const {items: accounts, EmptyState, LoadingState} = useInfiniteScrollTab({ + const {items: accounts, EmptyState, LoadingState} = useInfiniteScrollTab({ useDataHook: handle => useAccountFollowsForUser(handle, 'following'), emptyStateLabel: 'You aren\'t following anyone yet.', emptyStateIcon: 'user-add' @@ -223,7 +223,7 @@ const FollowingTab: React.FC = () => { }; const FollowersTab: React.FC = () => { - const {items: accounts, EmptyState, LoadingState} = useInfiniteScrollTab({ + const {items: accounts, EmptyState, LoadingState} = useInfiniteScrollTab({ useDataHook: handle => useAccountFollowsForUser(handle, 'followers'), emptyStateLabel: 'Nobody\'s following you yet. Their loss!', emptyStateIcon: 'user-add' diff --git a/apps/admin-x-activitypub/src/components/Search.tsx b/apps/admin-x-activitypub/src/components/Search.tsx index 663d324267..2c63000fab 100644 --- a/apps/admin-x-activitypub/src/components/Search.tsx +++ b/apps/admin-x-activitypub/src/components/Search.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useRef, useState} from 'react'; -import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub'; +import NiceModal from '@ebay/nice-modal-react'; import {Button, Icon, LoadingIndicator, NoValueLabel, TextField} from '@tryghost/admin-x-design-system'; import {useDebounce} from 'use-debounce'; @@ -8,60 +8,65 @@ import APAvatar from './global/APAvatar'; import ActivityItem from './activities/ActivityItem'; import FollowButton from './global/FollowButton'; import MainNavigation from './navigation/MainNavigation'; - -import NiceModal from '@ebay/nice-modal-react'; +import Separator from './global/Separator'; import ViewProfileModal from './modals/ViewProfileModal'; -import Separator from './global/Separator'; - +import {type Profile} from '../api/activitypub'; import {useSearchForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries'; -interface SearchResultItem { - actor: ActorProperties; +interface AccountSearchResult { + id: string; + name: string; handle: string; + avatarUrl: string; followerCount: number; - followingCount: number; - isFollowing: boolean; + followedByMe: boolean; } -interface SearchResultProps { - result: SearchResultItem; - update: (id: string, updated: Partial) => void; +interface AccountSearchResultItemProps { + account: AccountSearchResult; + update: (id: string, updated: Partial) => void; } -const SearchResult: React.FC = ({result, update}) => { +const AccountSearchResultItem: React.FC = ({account, update}) => { const onFollow = () => { - update(result.actor.id!, { - isFollowing: true, - followerCount: result.followerCount + 1 + update(account.id, { + followedByMe: true, + followerCount: account.followerCount + 1 }); }; const onUnfollow = () => { - update(result.actor.id!, { - isFollowing: false, - followerCount: result.followerCount - 1 + update(account.id, { + followedByMe: false, + followerCount: account.followerCount - 1 }); }; return ( { - NiceModal.show(ViewProfileModal, {handle: result.handle, onFollow, onUnfollow}); + NiceModal.show(ViewProfileModal, {handle: account.handle, onFollow, onUnfollow}); }} > - +
- {result.actor.name} {result.handle} + {account.name} {account.handle}
-
{new Intl.NumberFormat().format(result.followerCount)} followers
+
{new Intl.NumberFormat().format(account.followerCount)} followers
= ({result, update}) => { ); }; -const SearchResults: React.FC<{ - results: SearchResultItem[]; - onUpdate: (id: string, updated: Partial) => void; -}> = ({results, onUpdate}) => { +interface SearchResultsProps { + results: AccountSearchResult[]; + onUpdate: (id: string, updated: Partial) => void; +} + +const SearchResults: React.FC = ({results, onUpdate}) => { return ( <> - {results.map(result => ( - ( + ))} @@ -87,11 +94,59 @@ const SearchResults: React.FC<{ ); }; -const SuggestedAccounts: React.FC<{ - profiles: SearchResultItem[]; +interface SuggestedProfileProps { + profile: Profile; + update: (id: string, updated: Partial) => void; +} + +const SuggestedProfile: React.FC = ({profile, update}) => { + const onFollow = () => { + update(profile.actor.id, { + isFollowing: true, + followerCount: profile.followerCount + 1 + }); + }; + + const onUnfollow = () => { + update(profile.actor.id, { + isFollowing: false, + followerCount: profile.followerCount - 1 + }); + }; + + return ( + { + NiceModal.show(ViewProfileModal, {handle: profile.handle, onFollow, onUnfollow}); + }} + > + +
+
+ {profile.actor.name} {profile.handle} +
+
{new Intl.NumberFormat().format(profile.followerCount)} followers
+
+ +
+ ); +}; + +interface SuggestedProfilesProps { + profiles: Profile[]; isLoading: boolean; - onUpdate: (id: string, updated: Partial) => void; -}> = ({profiles, isLoading, onUpdate}) => { + onUpdate: (id: string, updated: Partial) => void; +} + +const SuggestedProfiles: React.FC = ({profiles, isLoading, onUpdate}) => { return ( <> @@ -105,9 +160,8 @@ const SuggestedAccounts: React.FC<{ {profiles.map((profile, index) => { return ( - {index < profiles.length - 1 && } @@ -123,17 +177,17 @@ interface SearchProps {} const Search: React.FC = ({}) => { // Initialise suggested profiles const {suggestedProfilesQuery, updateSuggestedProfile} = useSuggestedProfiles('index', 6); - const {data: suggestedData, isLoading: isLoadingSuggested} = suggestedProfilesQuery; - const suggested = suggestedData || []; + const {data: suggestedProfilesData, isLoading: isLoadingSuggestedProfiles} = suggestedProfilesQuery; + const suggestedProfiles = suggestedProfilesData || []; // Initialise search query const queryInputRef = useRef(null); const [query, setQuery] = useState(''); const [debouncedQuery] = useDebounce(query, 300); - const {searchQuery, updateProfileSearchResult: updateResult} = useSearchForUser('index', query !== '' ? debouncedQuery : query); + const {searchQuery, updateAccountSearchResult: updateResult} = useSearchForUser('index', query !== '' ? debouncedQuery : query); const {data, isFetching, isFetched} = searchQuery; - const results = data?.profiles || []; + const results = data?.accounts || []; const showLoading = isFetching && query.length > 0; const showNoResults = !isFetching && isFetched && results.length === 0 && query.length > 0 && debouncedQuery === query; const showSuggested = query === '' || (isFetched && results.length === 0); @@ -189,15 +243,15 @@ const Search: React.FC = ({}) => { {!showLoading && !showNoResults && ( )} {showSuggested && ( - )} diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 99c47e6c14..c700db6675 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -1,5 +1,6 @@ import { type AccountFollowsType, + type AccountSearchResult, ActivityPubAPI, ActivityPubCollectionResponse, ActivityThread, @@ -306,6 +307,7 @@ export function useSearchForUser(handle: string, query: string) { const searchQuery = useQuery({ queryKey, + enabled: query.length > 0, async queryFn() { const siteUrl = await getSiteUrl(); const api = createActivityPubAPI(handle, siteUrl); @@ -313,7 +315,7 @@ export function useSearchForUser(handle: string, query: string) { } }); - const updateProfileSearchResult = (id: string, updated: Partial) => { + const updateAccountSearchResult = (id: string, updated: Partial) => { queryClient.setQueryData(queryKey, (current: SearchResults | undefined) => { if (!current) { return current; @@ -321,8 +323,8 @@ export function useSearchForUser(handle: string, query: string) { return { ...current, - profiles: current.profiles.map((item: Profile) => { - if (item.actor.id === id) { + accounts: current.accounts.map((item: AccountSearchResult) => { + if (item.id === id) { return {...item, ...updated}; } return item; @@ -331,7 +333,7 @@ export function useSearchForUser(handle: string, query: string) { }); }; - return {searchQuery, updateProfileSearchResult}; + return {searchQuery, updateAccountSearchResult}; } export function useSuggestedProfiles(handle: string, limit = 3) {