0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Updated search results account type in admin-x-activitypub (#22021)

no refs

Updated the search results account type in the admin-x-activitypub to
match the updated API response
This commit is contained in:
Michael Barrett 2025-01-16 21:02:02 +00:00 committed by GitHub
parent e5ea3a0a8c
commit 026bb8ffbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 144 additions and 83 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@tryghost/admin-x-activitypub",
"version": "0.3.49",
"version": "0.3.50",
"license": "MIT",
"repository": {
"type": "git",

View file

@ -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);

View file

@ -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<string, string>;
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<string, string>;
postsCount: number;
likedCount: number;
followingCount: number;
followerCount: number;
followsMe: boolean;
followedByMe: boolean;
}
type GetAccountResponse = Account
export type MinimalAccount = Pick<Account, 'id' | 'name' | 'handle' | 'avatarUrl'>;
export type FollowAccount = Pick<Account, 'id' | 'name' | 'handle' | 'avatarUrl'>;
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: []
};
}

View file

@ -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<MinimalAccount>({
const {items: accounts, EmptyState, LoadingState} = useInfiniteScrollTab<FollowAccount>({
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<MinimalAccount>({
const {items: accounts, EmptyState, LoadingState} = useInfiniteScrollTab<FollowAccount>({
useDataHook: handle => useAccountFollowsForUser(handle, 'followers'),
emptyStateLabel: 'Nobody\'s following you yet. Their loss!',
emptyStateIcon: 'user-add'

View file

@ -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<SearchResultItem>) => void;
interface AccountSearchResultItemProps {
account: AccountSearchResult;
update: (id: string, updated: Partial<AccountSearchResult>) => void;
}
const SearchResult: React.FC<SearchResultProps> = ({result, update}) => {
const AccountSearchResultItem: React.FC<AccountSearchResultItemProps> = ({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 (
<ActivityItem
key={result.actor.id}
key={account.id}
onClick={() => {
NiceModal.show(ViewProfileModal, {handle: result.handle, onFollow, onUnfollow});
NiceModal.show(ViewProfileModal, {handle: account.handle, onFollow, onUnfollow});
}}
>
<APAvatar author={result.actor}/>
<APAvatar author={{
icon: {
url: account.avatarUrl
},
name: account.name,
handle: account.handle
}}/>
<div>
<div className='text-grey-600'>
<span className='font-bold text-black'>{result.actor.name} </span>{result.handle}
<span className='font-bold text-black'>{account.name} </span>{account.handle}
</div>
<div className='text-sm'>{new Intl.NumberFormat().format(result.followerCount)} followers</div>
<div className='text-sm'>{new Intl.NumberFormat().format(account.followerCount)} followers</div>
</div>
<FollowButton
className='ml-auto'
following={result.isFollowing}
handle={result.handle}
following={account.followedByMe}
handle={account.handle}
type='link'
onFollow={onFollow}
onUnfollow={onUnfollow}
@ -70,16 +75,18 @@ const SearchResult: React.FC<SearchResultProps> = ({result, update}) => {
);
};
const SearchResults: React.FC<{
results: SearchResultItem[];
onUpdate: (id: string, updated: Partial<SearchResultItem>) => void;
}> = ({results, onUpdate}) => {
interface SearchResultsProps {
results: AccountSearchResult[];
onUpdate: (id: string, updated: Partial<AccountSearchResult>) => void;
}
const SearchResults: React.FC<SearchResultsProps> = ({results, onUpdate}) => {
return (
<>
{results.map(result => (
<SearchResult
key={result.actor.id}
result={result}
{results.map(account => (
<AccountSearchResultItem
key={account.id}
account={account}
update={onUpdate}
/>
))}
@ -87,11 +94,59 @@ const SearchResults: React.FC<{
);
};
const SuggestedAccounts: React.FC<{
profiles: SearchResultItem[];
interface SuggestedProfileProps {
profile: Profile;
update: (id: string, updated: Partial<Profile>) => void;
}
const SuggestedProfile: React.FC<SuggestedProfileProps> = ({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 (
<ActivityItem
key={profile.actor.id}
onClick={() => {
NiceModal.show(ViewProfileModal, {handle: profile.handle, onFollow, onUnfollow});
}}
>
<APAvatar author={profile.actor}/>
<div>
<div className='text-grey-600'>
<span className='font-bold text-black'>{profile.actor.name} </span>{profile.handle}
</div>
<div className='text-sm'>{new Intl.NumberFormat().format(profile.followerCount)} followers</div>
</div>
<FollowButton
className='ml-auto'
following={profile.isFollowing}
handle={profile.handle}
type='link'
onFollow={onFollow}
onUnfollow={onUnfollow}
/>
</ActivityItem>
);
};
interface SuggestedProfilesProps {
profiles: Profile[];
isLoading: boolean;
onUpdate: (id: string, updated: Partial<SearchResultItem>) => void;
}> = ({profiles, isLoading, onUpdate}) => {
onUpdate: (id: string, updated: Partial<Profile>) => void;
}
const SuggestedProfiles: React.FC<SuggestedProfilesProps> = ({profiles, isLoading, onUpdate}) => {
return (
<>
<span className='mb-1 flex w-full max-w-[560px] font-semibold'>
@ -105,9 +160,8 @@ const SuggestedAccounts: React.FC<{
{profiles.map((profile, index) => {
return (
<React.Fragment key={profile.actor.id}>
<SearchResult
key={profile.actor.id}
result={profile}
<SuggestedProfile
profile={profile}
update={onUpdate}
/>
{index < profiles.length - 1 && <Separator />}
@ -123,17 +177,17 @@ interface SearchProps {}
const Search: React.FC<SearchProps> = ({}) => {
// 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<HTMLInputElement>(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<SearchProps> = ({}) => {
{!showLoading && !showNoResults && (
<SearchResults
results={results as SearchResultItem[]}
results={results}
onUpdate={updateResult}
/>
)}
{showSuggested && (
<SuggestedAccounts
isLoading={isLoadingSuggested}
profiles={suggested as SearchResultItem[]}
<SuggestedProfiles
isLoading={isLoadingSuggestedProfiles}
profiles={suggestedProfiles}
onUpdate={updateSuggestedProfile}
/>
)}

View file

@ -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<Profile>) => {
const updateAccountSearchResult = (id: string, updated: Partial<AccountSearchResult>) => {
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) {