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 (#21384)
refs [#479](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:
parent
ee980e3e34
commit
3d430b453f
7 changed files with 98 additions and 65 deletions
apps/admin-x-activitypub
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tryghost/admin-x-activitypub",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface Profile {
|
|||
actor: Actor;
|
||||
handle: string;
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
isFollowing: boolean;
|
||||
posts: Activity[];
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
}
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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}/>
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue