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:
parent
e5ea3a0a8c
commit
026bb8ffbf
6 changed files with 144 additions and 83 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tryghost/admin-x-activitypub",
|
||||
"version": "0.3.49",
|
||||
"version": "0.3.50",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: []
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue