Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated activitypub search suggestions to be dynamic #21202

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/admin-x-activitypub/src/api/activitypub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1091,4 +1091,41 @@ describe('ActivityPubAPI', function () {
expect(actual.following).toEqual([]);
});
});

describe('getProfile', function () {
test('It returns a profile', async function () {
const handle = '@[email protected]';

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);
});
});
});
10 changes: 8 additions & 2 deletions apps/admin-x-activitypub/src/api/activitypub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,7 +12,7 @@ export interface ProfileSearchResult {
}

export interface SearchResults {
profiles: ProfileSearchResult[];
profiles: Profile[];
}

export interface GetFollowersForProfileResponse {
Expand Down Expand Up @@ -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;
}
}
79 changes: 9 additions & 70 deletions apps/admin-x-activitypub/src/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: '@[email protected]',
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: '@[email protected]',
followerCount: 436,
followingCount: 634,
isFollowing: false,
posts: []
},
{
actor: {
id: 'https://flipboard.social/@miaq',
name: 'Mia Quagliarello',
preferredUsername: '@[email protected]',
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: '@[email protected]',
followerCount: 533,
followingCount: 335,
isFollowing: false,
posts: []
},
{
actor: {
id: 'https://techpolicy.social/@mallory',
name: 'Mallory',
preferredUsername: '@[email protected]',
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: '@[email protected]',
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', ['@[email protected]', '@[email protected]', '@[email protected]']);
const {data: suggested = [], isLoading: isLoadingSuggested} = suggestedProfilesQuery;

// Initialise search query
const queryInputRef = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -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}
/>
))}
</>
Expand Down
50 changes: 47 additions & 3 deletions apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -246,15 +246,15 @@ 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;
}

return {
...current,
profiles: current.profiles.map((item: ProfileSearchResult) => {
profiles: current.profiles.map((item: Profile) => {
if (item.actor.id === id) {
return {...item, ...updated};
}
Expand Down Expand Up @@ -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);
}
});
}
Loading