0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Opened follow notifications in profile viewer in admin-x-activitypub ()

refs
[](https://linear.app/ghost/issue/AP-479/open-profile-viewer-when-clicking-follow-notifications)

Clicking on a follow notification will now open the profile viewer with
details of the profile of the user that followed you

---------

Co-authored-by: Djordje Vlaisavljevic <dzvlais@gmail.com>
This commit is contained in:
Michael Barrett 2024-10-23 14:26:31 +01:00 committed by GitHub
parent ee980e3e34
commit 3d430b453f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 98 additions and 65 deletions

View file

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

View file

@ -7,6 +7,7 @@ export interface Profile {
actor: Actor;
handle: string;
followerCount: number;
followingCount: number;
isFollowing: boolean;
posts: Activity[];
}

View file

@ -8,6 +8,7 @@ import ActivityItem, {type Activity} from './activities/ActivityItem';
import ArticleModal from './feed/ArticleModal';
// import FollowButton from './global/FollowButton';
import MainNavigation from './navigation/MainNavigation';
import ViewProfileModal from './global/ViewProfileModal';
import getUsername from '../utils/get-username';
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
@ -155,6 +156,11 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
});
break;
case ACTVITY_TYPE.FOLLOW:
NiceModal.show(ViewProfileModal, {
profile: getUsername(activity.actor),
onFollow: () => {},
onUnfollow: () => {}
});
break;
default:
}

View file

@ -6,6 +6,7 @@ import FeedItem from './feed/FeedItem';
import MainNavigation from './navigation/MainNavigation';
import NiceModal from '@ebay/nice-modal-react';
import React, {useEffect, useRef, useState} from 'react';
import ViewProfileModal from './global/ViewProfileModal';
import getUsername from '../utils/get-username';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system';
@ -165,7 +166,11 @@ const Inbox: React.FC<InboxProps> = ({}) => {
// const isFollowing = profile.isFollowing;
return (
<li key={actor.id}>
<ActivityItem url={actor.url}>
<ActivityItem url={actor.url} onClick={() => NiceModal.show(ViewProfileModal, {
profile: getUsername(actor),
onFollow: () => {},
onUnfollow: () => {}
})}>
<APAvatar author={actor} />
<div>
<div className='text-grey-600'>

View file

@ -10,7 +10,7 @@ import FollowButton from './global/FollowButton';
import MainNavigation from './navigation/MainNavigation';
import NiceModal from '@ebay/nice-modal-react';
import ProfileSearchResultModal from './search/ProfileSearchResultModal';
import ViewProfileModal from './global/ViewProfileModal';
import {useSearchForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries';
@ -49,7 +49,7 @@ const SearchResult: React.FC<SearchResultProps> = ({result, update}) => {
<ActivityItem
key={result.actor.id}
onClick={() => {
NiceModal.show(ProfileSearchResultModal, {profile: result, onFollow, onUnfollow});
NiceModal.show(ViewProfileModal, {profile: result, onFollow, onUnfollow});
}}
>
<APAvatar author={result.actor}/>

View file

@ -7,7 +7,7 @@ import {Button, Heading, Icon, List, LoadingIndicator, Modal, NoValueLabel, Tab,
import {UseInfiniteQueryResult} from '@tanstack/react-query';
import {type GetFollowersForProfileResponse, type GetFollowingForProfileResponse} from '../../api/activitypub';
import {useFollowersForProfile, useFollowingForProfile} from '../../hooks/useActivityPubQueries';
import {useFollowersForProfile, useFollowingForProfile, useProfileForUser} from '../../hooks/useActivityPubQueries';
import APAvatar from '../global/APAvatar';
import ActivityItem from '../activities/ActivityItem';
@ -136,7 +136,7 @@ const FollowingTab: React.FC<{handle: string}> = ({handle}) => {
);
};
interface ProfileSearchResultModalProps {
interface ViewProfileModalProps {
profile: {
actor: ActorProperties;
handle: string;
@ -144,25 +144,33 @@ interface ProfileSearchResultModalProps {
followingCount: number;
isFollowing: boolean;
posts: Activity[];
};
} | string;
onFollow: () => void;
onUnfollow: () => void;
}
type ProfileTab = 'posts' | 'following' | 'followers';
const ProfileSearchResultModal: React.FC<ProfileSearchResultModalProps> = ({
profile,
const ViewProfileModal: React.FC<ViewProfileModalProps> = ({
profile: initialProfile,
onFollow = noop,
onUnfollow = noop
}) => {
const modal = useModal();
const [selectedTab, setSelectedTab] = useState<ProfileTab>('posts');
const attachments = (profile.actor.attachment || []);
const posts = (profile.posts || []).filter(post => post.type !== 'Announce');
const willLoadProfile = typeof initialProfile === 'string';
let {data: profile, isInitialLoading: isLoading} = useProfileForUser('index', initialProfile as string, willLoadProfile);
const tabs = [
if (!willLoadProfile) {
profile = initialProfile;
isLoading = false;
}
const attachments = (profile?.actor.attachment || []);
const posts = (profile?.posts || []).filter(post => post.type !== 'Announce');
const tabs = isLoading === false && typeof profile !== 'string' && profile ? [
{
id: 'posts',
title: 'Posts',
@ -202,7 +210,7 @@ const ProfileSearchResultModal: React.FC<ProfileSearchResultModalProps> = ({
),
counter: profile.followerCount
}
].filter(Boolean) as Tab<ProfileTab>[];
].filter(Boolean) as Tab<ProfileTab>[] : [];
const [isExpanded, setisExpanded] = useState(false);
@ -238,62 +246,74 @@ const ProfileSearchResultModal: React.FC<ProfileSearchResultModalProps> = ({
</div>
<div className='z-0 mx-auto mt-4 flex w-full max-w-[580px] flex-col items-center pb-16'>
<div className='mx-auto w-full'>
{profile.actor.image && (<div className='h-[200px] w-full overflow-hidden rounded-lg bg-gradient-to-tr from-grey-200 to-grey-100'>
<img
alt={profile.actor.name}
className='h-full w-full object-cover'
src={profile.actor.image.url}
/>
</div>)}
<div className={`${profile.actor.image && '-mt-12'} px-4`}>
<div className='flex items-end justify-between'>
<div className='rounded-xl outline outline-4 outline-white'>
<APAvatar
author={profile.actor}
size='lg'
{isLoading && (
<LoadingIndicator size='lg' />
)}
{!isLoading && !profile && (
<NoValueLabel icon='user-add'>
Profile not found
</NoValueLabel>
)}
{!isLoading && profile && (
<>
{profile.actor.image && (<div className='h-[200px] w-full overflow-hidden rounded-lg bg-gradient-to-tr from-grey-200 to-grey-100'>
<img
alt={profile.actor.name}
className='h-full w-full object-cover'
src={profile.actor.image.url}
/>
</div>)}
<div className={`${profile.actor.image && '-mt-12'} px-4`}>
<div className='flex items-end justify-between'>
<div className='rounded-xl outline outline-4 outline-white'>
<APAvatar
author={profile.actor}
size='lg'
/>
</div>
<FollowButton
following={profile.isFollowing}
handle={profile.handle}
onFollow={onFollow}
onUnfollow={onUnfollow}
/>
</div>
<Heading className='mt-4' level={3}>{profile.actor.name}</Heading>
<a className='group/handle mt-1 flex items-center gap-1 text-[1.5rem] text-grey-800 hover:text-grey-900' href={profile?.actor.url} rel='noopener noreferrer' target='_blank'><span>{profile.handle}</span><Icon className='opacity-0 transition-opacity group-hover/handle:opacity-100' name='arrow-top-right' size='xs'/></a>
{(profile.actor.summary || attachments.length > 0) && (<div ref={contentRef} className={`ap-profile-content transition-max-height relative text-[1.5rem] duration-300 ease-in-out [&>p]:mb-3 ${isExpanded ? 'max-h-none pb-7' : 'max-h-[160px] overflow-hidden'} relative`}>
<div
dangerouslySetInnerHTML={{__html: profile.actor.summary}}
className='ap-profile-content mt-3 text-[1.5rem] [&>p]:mb-3'
/>
{attachments.map((attachment: {name: string, value: string}) => (
<span className='mt-3 line-clamp-1 flex flex-col text-[1.5rem]'>
<span className={`text-xs font-semibold`}>{attachment.name}</span>
<span dangerouslySetInnerHTML={{__html: attachment.value}} className='ap-profile-content truncate'/>
</span>
))}
{!isExpanded && isOverflowing && (
<div className='absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white via-white/90 via-60% to-transparent' />
)}
{isOverflowing && <Button
className='absolute bottom-0 text-pink'
label={isExpanded ? 'Show less' : 'Show all'}
link={true}
onClick={toggleExpand}
/>}
</div>)}
<TabView<ProfileTab>
containerClassName='mt-6'
selectedTab={selectedTab}
tabs={tabs}
onTabChange={setSelectedTab}
/>
</div>
<FollowButton
following={profile.isFollowing}
handle={profile.handle}
onFollow={onFollow}
onUnfollow={onUnfollow}
/>
</div>
<Heading className='mt-4' level={3}>{profile.actor.name}</Heading>
<a className='group/handle mt-1 flex items-center gap-1 text-[1.5rem] text-grey-800 hover:text-grey-900' href={profile?.actor.url} rel='noopener noreferrer' target='_blank'><span>{profile.handle}</span><Icon className='opacity-0 transition-opacity group-hover/handle:opacity-100' name='arrow-top-right' size='xs'/></a>
{(profile.actor.summary || attachments.length > 0) && (<div ref={contentRef} className={`ap-profile-content transition-max-height relative text-[1.5rem] duration-300 ease-in-out [&>p]:mb-3 ${isExpanded ? 'max-h-none pb-7' : 'max-h-[160px] overflow-hidden'} relative`}>
<div
dangerouslySetInnerHTML={{__html: profile.actor.summary}}
className='ap-profile-content mt-3 text-[1.5rem] [&>p]:mb-3'
/>
{attachments.map(attachment => (
<span className='mt-3 line-clamp-1 flex flex-col text-[1.5rem]'>
<span className={`text-xs font-semibold`}>{attachment.name}</span>
<span dangerouslySetInnerHTML={{__html: attachment.value}} className='ap-profile-content truncate'/>
</span>
))}
{!isExpanded && isOverflowing && (
<div className='absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white via-white/90 via-60% to-transparent' />
)}
{isOverflowing && <Button
className='absolute bottom-0 text-pink'
label={isExpanded ? 'Show less' : 'Show all'}
link={true}
onClick={toggleExpand}
/>}
</div>)}
<TabView<ProfileTab>
containerClassName='mt-6'
selectedTab={selectedTab}
tabs={tabs}
onTabChange={setSelectedTab}
/>
</div>
</>
)}
</div>
</div>
</Modal>
);
};
export default NiceModal.create(ProfileSearchResultModal);
export default NiceModal.create(ViewProfileModal);

View file

@ -363,9 +363,10 @@ export function useSuggestedProfiles(handle: string, handles: string[]) {
return {suggestedProfilesQuery, updateSuggestedProfile};
}
export function useProfileForUser(handle: string, fullHandle: string) {
export function useProfileForUser(handle: string, fullHandle: string, enabled: boolean = true) {
return useQuery({
queryKey: [`profile:${fullHandle}`],
enabled,
async queryFn() {
const siteUrl = await getSiteUrl();
const api = createActivityPubAPI(handle, siteUrl);