0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Updated activitypub search suggestions to be dynamic (#21202)

refs
[TryGhost/ActivityPub#60](https://github.com/TryGhost/ActivityPub/pull/60)

Updated activitypub search suggestions to be dynamic
This commit is contained in:
Michael Barrett 2024-10-03 14:43:54 +01:00 committed by GitHub
parent 1196688b0e
commit 0d8ea553bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 75 deletions

View file

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

View file

@ -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<Profile> {
const url = new URL(`.ghost/activitypub/profile/${handle}`, this.apiUrl);
const json = await this.fetchJSON(url);
return json as Profile;
}
}

View file

@ -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<SearchResultProps> = ({result, update}) => {
const Search: React.FC<SearchProps> = ({}) => {
// Initialise suggested profiles
const [suggested, setSuggested] = useState<SearchResultItem[]>([
{
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<SearchResultItem>) => {
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<HTMLInputElement>(null);
@ -220,11 +156,14 @@ const Search: React.FC<SearchProps> = ({}) => {
{showSuggested && (
<>
<span className='mb-1 flex w-full max-w-[560px] font-semibold'>Suggested accounts</span>
{isLoadingSuggested && (
<LoadingIndicator size='sm'/>
)}
{suggested.map(profile => (
<SearchResult
key={profile.actor.id}
result={profile}
update={updateSuggested}
key={(profile as SearchResultItem).actor.id}
result={profile as SearchResultItem}
update={updateSuggestedProfile}
/>
))}
</>

View file

@ -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<ProfileSearchResult>) => {
const updateProfileSearchResult = (id: string, updated: Partial<Profile>) => {
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<Profile>) => {
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);
}
});
}