mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Refactored and reorganized FeedItem
and Profile
(#20919)
- Moved engagement stats to a reusable component - Moved functions from Profile to a separate file - Fixed Following on Your Profile and moved them from modals to tabs
This commit is contained in:
parent
426b1d4d93
commit
21fb57eabd
9 changed files with 262 additions and 141 deletions
|
@ -108,7 +108,7 @@ describe('ActivityPubAPI', function () {
|
|||
response:
|
||||
JSONResponse({
|
||||
type: 'Collection',
|
||||
items: [{
|
||||
orderedItems: [{
|
||||
type: 'Create',
|
||||
object: {
|
||||
type: 'Note'
|
||||
|
@ -138,7 +138,7 @@ describe('ActivityPubAPI', function () {
|
|||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
test('Returns an array when the items key is a single object', async function () {
|
||||
test('Returns an array when the orderedItems key is a single object', async function () {
|
||||
const fakeFetch = Fetch({
|
||||
'https://auth.api/': {
|
||||
response: JSONResponse({
|
||||
|
@ -151,7 +151,7 @@ describe('ActivityPubAPI', function () {
|
|||
response:
|
||||
JSONResponse({
|
||||
type: 'Collection',
|
||||
items: {
|
||||
orderedItems: {
|
||||
type: 'Create',
|
||||
object: {
|
||||
type: 'Note'
|
||||
|
@ -255,7 +255,7 @@ describe('ActivityPubAPI', function () {
|
|||
response:
|
||||
JSONResponse({
|
||||
type: 'Collection',
|
||||
items: [{
|
||||
orderedItems: [{
|
||||
type: 'Person'
|
||||
}]
|
||||
})
|
||||
|
|
|
@ -44,6 +44,9 @@ export class ActivityPubAPI {
|
|||
if (json === null) {
|
||||
return [];
|
||||
}
|
||||
if ('orderedItems' in json) {
|
||||
return Array.isArray(json.orderedItems) ? json.orderedItems : [json.orderedItems];
|
||||
}
|
||||
if ('items' in json) {
|
||||
return Array.isArray(json.items) ? json.items : [json.items];
|
||||
}
|
||||
|
@ -59,6 +62,9 @@ export class ActivityPubAPI {
|
|||
if (json === null) {
|
||||
return [];
|
||||
}
|
||||
if ('orderedItems' in json) {
|
||||
return Array.isArray(json.orderedItems) ? json.orderedItems : [json.orderedItems];
|
||||
}
|
||||
if ('items' in json) {
|
||||
return Array.isArray(json.items) ? json.items : [json.items];
|
||||
}
|
||||
|
@ -86,7 +92,7 @@ export class ActivityPubAPI {
|
|||
return [];
|
||||
}
|
||||
if ('orderedItems' in json) {
|
||||
return json.orderedItems as Activity[];
|
||||
return Array.isArray(json.orderedItems) ? json.orderedItems : [json.orderedItems];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -106,4 +112,9 @@ export class ActivityPubAPI {
|
|||
const url = new URL(`.ghost/activitypub/actions/follow/${username}`, this.apiUrl);
|
||||
await this.fetchJSON(url, 'POST');
|
||||
}
|
||||
|
||||
async getActor(url: string): Promise<Actor> {
|
||||
const json = await this.fetchJSON(new URL(url));
|
||||
return json as Actor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ const Inbox: React.FC<InboxProps> = ({}) => {
|
|||
type={activity.type}
|
||||
/>
|
||||
{index < inboxTabActivities.length - 1 && (
|
||||
<div className="my-4 h-px w-full bg-grey-200"></div>
|
||||
<div className="h-px w-full bg-grey-200"></div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
|
|
|
@ -1,50 +1,18 @@
|
|||
import APAvatar from './global/APAvatar';
|
||||
import ActivityItem from './activities/ActivityItem';
|
||||
import MainNavigation from './navigation/MainNavigation';
|
||||
import React, {useState} from 'react';
|
||||
import {ActivityPubAPI} from '../api/activitypub';
|
||||
import {Heading, NoValueLabel, Tab, TabView} from '@tryghost/admin-x-design-system';
|
||||
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
import getUsername from '../utils/get-username';
|
||||
import {Button, Heading, List, NoValueLabel, Tab, TabView} from '@tryghost/admin-x-design-system';
|
||||
import {useFollowersCountForUser, useFollowersForUser, useFollowingCountForUser, useFollowingForUser} from '../hooks/useActivityPubQueries';
|
||||
|
||||
interface ProfileProps {}
|
||||
|
||||
function useFollowersCountForUser(handle: string) {
|
||||
const site = useBrowseSite();
|
||||
const siteData = site.data?.site;
|
||||
const siteUrl = siteData?.url ?? window.location.origin;
|
||||
const api = new ActivityPubAPI(
|
||||
new URL(siteUrl),
|
||||
new URL('/ghost/api/admin/identities/', window.location.origin),
|
||||
handle
|
||||
);
|
||||
return useQuery({
|
||||
queryKey: [`followersCount:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowersCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useFollowingCountForUser(handle: string) {
|
||||
const site = useBrowseSite();
|
||||
const siteData = site.data?.site;
|
||||
const siteUrl = siteData?.url ?? window.location.origin;
|
||||
const api = new ActivityPubAPI(
|
||||
new URL(siteUrl),
|
||||
new URL('/ghost/api/admin/identities/', window.location.origin),
|
||||
handle
|
||||
);
|
||||
return useQuery({
|
||||
queryKey: [`followingCount:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowingCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const Profile: React.FC<ProfileProps> = ({}) => {
|
||||
const {data: followersCount = 0} = useFollowersCountForUser('index');
|
||||
const {data: followingCount = 0} = useFollowingCountForUser('index');
|
||||
const {data: following = []} = useFollowingForUser('index');
|
||||
const {data: followers = []} = useFollowersForUser('index');
|
||||
|
||||
type ProfileTab = 'posts' | 'likes' | 'following' | 'followers';
|
||||
|
||||
|
@ -55,7 +23,7 @@ const Profile: React.FC<ProfileProps> = ({}) => {
|
|||
id: 'posts',
|
||||
title: 'Posts',
|
||||
contents: (<div><NoValueLabel icon='pen'>
|
||||
You haven’t posted anything yet.
|
||||
You haven't posted anything yet.
|
||||
</NoValueLabel></div>),
|
||||
counter: 240
|
||||
},
|
||||
|
@ -63,24 +31,72 @@ const Profile: React.FC<ProfileProps> = ({}) => {
|
|||
id: 'likes',
|
||||
title: 'Likes',
|
||||
contents: (<div><NoValueLabel icon='heart'>
|
||||
You haven’t liked anything yet.
|
||||
You haven't liked anything yet.
|
||||
</NoValueLabel></div>),
|
||||
counter: 27
|
||||
},
|
||||
{
|
||||
id: 'following',
|
||||
title: 'Following',
|
||||
contents: (<div><NoValueLabel icon='user-add'>
|
||||
You haven’t followed anyone yet.
|
||||
</NoValueLabel></div>),
|
||||
contents: (
|
||||
<div>
|
||||
{following.length === 0 ? (
|
||||
<NoValueLabel icon='user-add'>
|
||||
You haven't followed anyone yet.
|
||||
</NoValueLabel>
|
||||
) : (
|
||||
<List>
|
||||
{following.map((item) => {
|
||||
return (
|
||||
<ActivityItem key={item.id} url={item.url}>
|
||||
<APAvatar author={item} />
|
||||
<div>
|
||||
<div className='text-grey-600'>
|
||||
<span className='mr-1 font-bold text-black'>{item.name || item.preferredUsername || 'Unknown'}</span>
|
||||
<div className='text-sm'>{getUsername(item)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button className='ml-auto' color='grey' label='Unfollow' link={true} onClick={(e) => {
|
||||
e?.preventDefault();
|
||||
alert('Implement me!');
|
||||
}} />
|
||||
</ActivityItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
counter: followingCount
|
||||
},
|
||||
{
|
||||
id: 'followers',
|
||||
title: 'Followers',
|
||||
contents: (<div><NoValueLabel icon='user-add'>
|
||||
Nobody’s following you yet. Their loss!
|
||||
</NoValueLabel></div>),
|
||||
contents: (
|
||||
<div>
|
||||
{followers.length === 0 ? (
|
||||
<NoValueLabel icon='user-add'>
|
||||
Nobody's following you yet. Their loss!
|
||||
</NoValueLabel>
|
||||
) : (
|
||||
<List>
|
||||
{followers.map((item) => {
|
||||
return (
|
||||
<ActivityItem key={item.id} url={item.url}>
|
||||
<APAvatar author={item} />
|
||||
<div>
|
||||
<div className='text-grey-600'>
|
||||
<span className='mr-1 font-bold text-black'>{item.name || item.preferredUsername || 'Unknown'}</span>
|
||||
<div className='text-sm'>{getUsername(item)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ActivityItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
counter: followersCount
|
||||
}
|
||||
].filter(Boolean) as Tab<ProfileTab>[];
|
||||
|
@ -102,17 +118,6 @@ const Profile: React.FC<ProfileProps> = ({}) => {
|
|||
<a className='mt-3 block text-[1.5rem] underline' href='#'>www.coolsite.com</a>
|
||||
<TabView<'posts' | 'likes' | 'following' | 'followers'> containerClassName='mt-6' selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
||||
</div>
|
||||
|
||||
{/* <div className='grid grid-cols-2 gap-4'>
|
||||
<div className='group/stat flex cursor-pointer flex-col gap-1' onClick={() => updateRoute('/profile/following')}>
|
||||
<span className='text-3xl font-bold leading-none' data-test-following-count>{followingCount}</span>
|
||||
<span className='text-base leading-none text-grey-800 group-hover/stat:text-grey-900' data-test-following-modal>Following<span className='ml-1 opacity-0 transition-opacity group-hover/stat:opacity-100'>→</span></span>
|
||||
</div>
|
||||
<div className='group/stat flex cursor-pointer flex-col gap-1' onClick={() => updateRoute('/profile/followers')}>
|
||||
<span className='text-3xl font-bold leading-none' data-test-following-count>{followersCount}</span>
|
||||
<span className='text-base leading-none text-grey-800 group-hover/stat:text-grey-900' data-test-followers-modal>Followers<span className='ml-1 opacity-0 transition-opacity group-hover/stat:opacity-100'>→</span></span>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -126,6 +126,60 @@ function renderInboxAttachment(object: ObjectProperties) {
|
|||
}
|
||||
}
|
||||
|
||||
const FeedItemStats: React.FC<{
|
||||
isLiked: boolean;
|
||||
likeCount: number;
|
||||
commentCount: number;
|
||||
onLikeClick: () => void;
|
||||
onCommentClick: () => void;
|
||||
}> = ({isLiked: initialLikedState, likeCount, commentCount, onLikeClick, onCommentClick}) => {
|
||||
const [isClicked, setIsClicked] = useState(false);
|
||||
const [isLiked, setIsLiked] = useState(initialLikedState);
|
||||
|
||||
const handleLikeClick = () => {
|
||||
setIsClicked(true);
|
||||
setIsLiked(!isLiked);
|
||||
|
||||
// Call the requested `onLikeClick`
|
||||
onLikeClick();
|
||||
|
||||
setTimeout(() => setIsClicked(false), 300);
|
||||
};
|
||||
|
||||
return (<div className='flex gap-5'>
|
||||
<div className='mt-3 flex gap-1'>
|
||||
<Button
|
||||
className={`self-start text-grey-900 transition-all hover:opacity-70 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`}
|
||||
hideLabel={true}
|
||||
icon='heart'
|
||||
id='like'
|
||||
size='md'
|
||||
unstyled={true}
|
||||
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
||||
e?.stopPropagation();
|
||||
handleLikeClick();
|
||||
}}
|
||||
/>
|
||||
{isLiked && <span className={`text-grey-900`}>{likeCount}</span>}
|
||||
</div>
|
||||
<div className='mt-3 flex gap-1'>
|
||||
<Button
|
||||
className={`self-start text-grey-900`}
|
||||
hideLabel={true}
|
||||
icon='comment'
|
||||
id='comment'
|
||||
size='md'
|
||||
unstyled={true}
|
||||
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
||||
e?.stopPropagation();
|
||||
onCommentClick();
|
||||
}}
|
||||
/>
|
||||
<span className={`text-grey-900`}>{commentCount}</span>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
|
||||
interface FeedItemProps {
|
||||
actor: ActorProperties;
|
||||
object: ObjectProperties;
|
||||
|
@ -140,14 +194,10 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||
|
||||
const date = new Date(object?.published ?? new Date());
|
||||
|
||||
const [isClicked, setIsClicked] = useState(false);
|
||||
const [isLiked, setIsLiked] = useState(false);
|
||||
|
||||
const handleLikeClick = (event: React.MouseEvent<HTMLElement> | undefined) => {
|
||||
event?.stopPropagation();
|
||||
setIsClicked(true);
|
||||
setIsLiked(!isLiked);
|
||||
setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms
|
||||
const isLiked = false;
|
||||
const onLikeClick = () => {
|
||||
// Do API req or smth
|
||||
// Don't need to know about setting timeouts or anything like that
|
||||
};
|
||||
|
||||
let author = actor;
|
||||
|
@ -159,15 +209,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||
return (
|
||||
<>
|
||||
{object && (
|
||||
<div className={`group/article relative cursor-pointer pt-5`}>
|
||||
<div className={`group/article relative cursor-pointer pt-6`}>
|
||||
{(type === 'Announce' && object.type === 'Note') && <div className='z-10 mb-2 flex items-center gap-3 text-grey-700'>
|
||||
<div className='z-10 flex w-10 justify-end'><Icon colorClass='text-grey-700' name='reload' size={'sm'}></Icon></div>
|
||||
<span className='z-10'>{actor.name} reposted</span>
|
||||
</div>}
|
||||
<div className={`border-1 z-10 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-x-3 gap-y-2 border-b-grey-200 pb-4`} data-test-activity>
|
||||
<div className='relative z-10 pt-[3px]'>
|
||||
<APAvatar author={author}/>
|
||||
</div>
|
||||
<div className={`border-1 z-10 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-x-3 gap-y-2 border-b-grey-200 pb-6`} data-test-activity>
|
||||
<APAvatar author={author}/>
|
||||
{/* <div className='border-1 z-10 -mt-1 flex w-full flex-col items-start justify-between border-b border-b-grey-200 pb-4' data-test-activity> */}
|
||||
<div className='relative z-10 flex w-full flex-col overflow-visible text-[1.5rem]'>
|
||||
<div className='flex'>
|
||||
|
@ -183,10 +231,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||
{object.name && <Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>}
|
||||
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
|
||||
{renderFeedAttachment(object, layout)}
|
||||
<div className='mt-3 flex gap-2'>
|
||||
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id='like' size='md' unstyled={true} onClick={handleLikeClick}/>
|
||||
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
|
||||
</div>
|
||||
<FeedItemStats
|
||||
commentCount={2}
|
||||
isLiked={isLiked}
|
||||
likeCount={1}
|
||||
onCommentClick={onLikeClick}
|
||||
onLikeClick={onLikeClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
|
@ -225,14 +276,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||
{object.name && <Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>}
|
||||
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.6rem] text-grey-900'></div>
|
||||
{renderFeedAttachment(object, layout)}
|
||||
{/* <div className='mt-3 flex gap-2'>
|
||||
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id='like' size='md' unstyled={true} onClick={handleLikeClick}/>
|
||||
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
|
||||
</div> */}
|
||||
<div className='mt-3 flex gap-1'>
|
||||
<Button className={`self-start text-grey-900`} hideLabel={true} icon='comment' id='like' size='md' unstyled={true} onClick={handleLikeClick}/>
|
||||
<span className={`text-grey-900`}>2</span>
|
||||
</div>
|
||||
<FeedItemStats
|
||||
commentCount={2}
|
||||
isLiked={isLiked}
|
||||
likeCount={1}
|
||||
onCommentClick={onLikeClick}
|
||||
onLikeClick={onLikeClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
|
@ -273,10 +323,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||
{object.name && <Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>}
|
||||
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
|
||||
{renderFeedAttachment(object, layout)}
|
||||
<div className='mt-3 flex gap-1'>
|
||||
<Button className={`self-start text-grey-900`} hideLabel={true} icon='comment' id='like' size='md' unstyled={true} onClick={handleLikeClick}/>
|
||||
<span className={`text-grey-900`}>1</span>
|
||||
</div>
|
||||
<FeedItemStats
|
||||
commentCount={2}
|
||||
isLiked={isLiked}
|
||||
likeCount={1}
|
||||
onCommentClick={onLikeClick}
|
||||
onLikeClick={onLikeClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
|
@ -307,6 +360,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, last})
|
|||
</div>
|
||||
{renderInboxAttachment(object)}
|
||||
</div>
|
||||
<FeedItemStats
|
||||
commentCount={2}
|
||||
isLiked={isLiked}
|
||||
likeCount={1}
|
||||
onCommentClick={onLikeClick}
|
||||
onLikeClick={onLikeClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
|
|||
return (
|
||||
<>
|
||||
{author && author.icon?.url ? (
|
||||
<div className='relative'>
|
||||
<a className='relative z-10 pt-[3px] transition-opacity hover:opacity-80' href={author.url} rel='noopener noreferrer' target='_blank'>
|
||||
<img
|
||||
className={imageClass}
|
||||
src={author.icon.url}
|
||||
|
@ -61,7 +61,7 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div className={containerClass}>
|
||||
<Icon
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import getUsername from '../../utils/get-username';
|
||||
import {ActivityPubAPI} from '../../api/activitypub';
|
||||
import {Avatar, Button, List, ListItem, Modal} from '@tryghost/admin-x-design-system';
|
||||
import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
|
||||
import {useMutation, useQuery} from '@tanstack/react-query';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
|
||||
function useFollowersForUser(handle: string) {
|
||||
const site = useBrowseSite();
|
||||
|
@ -18,39 +19,21 @@ function useFollowersForUser(handle: string) {
|
|||
return useQuery({
|
||||
queryKey: [`followers:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useFollow(handle: string) {
|
||||
const site = useBrowseSite();
|
||||
const siteData = site.data?.site;
|
||||
const siteUrl = siteData?.url ?? window.location.origin;
|
||||
const api = new ActivityPubAPI(
|
||||
new URL(siteUrl),
|
||||
new URL('/ghost/api/admin/identities/', window.location.origin),
|
||||
handle
|
||||
);
|
||||
return useMutation({
|
||||
async mutationFn(username: string) {
|
||||
return api.follow(username);
|
||||
const followerUrls = await api.getFollowers();
|
||||
const followerActors = await Promise.all(followerUrls.map(url => api.getActor(url)));
|
||||
return followerActors;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const ViewFollowersModal: React.FC<RoutingModalProps> = ({}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
// const modal = NiceModal.useModal();
|
||||
const mutation = useFollow('index');
|
||||
|
||||
const {data: items = []} = useFollowersForUser('index');
|
||||
const {data: followers = [], isLoading} = useFollowersForUser('index');
|
||||
|
||||
const followers = Array.isArray(items) ? items : [items];
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
mutation.reset();
|
||||
updateRoute('profile');
|
||||
}}
|
||||
cancelLabel=''
|
||||
|
@ -61,11 +44,22 @@ const ViewFollowersModal: React.FC<RoutingModalProps> = ({}) => {
|
|||
topRightContent='close'
|
||||
>
|
||||
<div className='mt-3 flex flex-col gap-4 pb-12'>
|
||||
<List>
|
||||
{followers.map(item => (
|
||||
<ListItem action={<Button color='grey' label='Follow back' link={true} onClick={() => mutation.mutate(getUsername(item))} />} avatar={<Avatar image={item.icon} size='sm' />} detail={getUsername(item)} id='list-item' title={item.name}></ListItem>
|
||||
))}
|
||||
</List>
|
||||
{isLoading ? (
|
||||
<p>Loading followers...</p>
|
||||
) : (
|
||||
<List>
|
||||
{followers.map(item => (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
action={<Button color='grey' label='Remove' link={true} />}
|
||||
avatar={<Avatar image={item.icon} size='sm' />}
|
||||
detail={getUsername(item)}
|
||||
id='list-item'
|
||||
title={item.name || getUsername(item)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
@ -26,9 +26,8 @@ function useFollowingForUser(handle: string) {
|
|||
const ViewFollowingModal: React.FC<RoutingModalProps> = ({}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const {data: items = []} = useFollowingForUser('index');
|
||||
const {data: following = []} = useFollowingForUser('index');
|
||||
|
||||
const following = Array.isArray(items) ? items : [items];
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
|
@ -44,25 +43,16 @@ const ViewFollowingModal: React.FC<RoutingModalProps> = ({}) => {
|
|||
<div className='mt-3 flex flex-col gap-4 pb-12'>
|
||||
<List>
|
||||
{following.map(item => (
|
||||
<ListItem action={<Button color='grey' label='Unfollow' link={true} />} avatar={<Avatar image={item.icon} size='sm' />} detail={getUsername(item)} id='list-item' title={item.name}></ListItem>
|
||||
<ListItem
|
||||
key={item.id} // Add a key prop
|
||||
action={<Button color='grey' label='Unfollow' link={true} />}
|
||||
avatar={<Avatar image={item.icon} size='sm' />}
|
||||
detail={getUsername(item)}
|
||||
id='list-item'
|
||||
title={item.name}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
{/* <Table>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className='group flex items-center gap-3 hover:cursor-pointer'>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<div className="mb-0.5 flex items-center gap-3">
|
||||
<img className='w-5' src='https://www.platformer.news/content/images/size/w256h256/2024/05/Logomark_Blue_800px.png'/>
|
||||
<span className='line-clamp-1 font-medium'>Platformer Platformer Platformer Platformer Platformer</span>
|
||||
<span className='line-clamp-1'>@index@platformerplatformerplatformerplatformer.news</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className='w-[1%] whitespace-nowrap'><div className='mt-1 whitespace-nowrap text-right text-sm text-grey-700'>Unfollow</div></TableCell>
|
||||
</TableRow>
|
||||
</Table> */}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
61
apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts
Normal file
61
apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import {ActivityPubAPI} from '../api/activitypub';
|
||||
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
|
||||
import {useQuery} from '@tanstack/react-query';
|
||||
|
||||
const useSiteUrl = () => {
|
||||
const site = useBrowseSite();
|
||||
return site.data?.site?.url ?? window.location.origin;
|
||||
};
|
||||
|
||||
function createActivityPubAPI(handle: string, siteUrl: string) {
|
||||
return new ActivityPubAPI(
|
||||
new URL(siteUrl),
|
||||
new URL('/ghost/api/admin/identities/', window.location.origin),
|
||||
handle
|
||||
);
|
||||
}
|
||||
|
||||
export function useFollowersCountForUser(handle: string) {
|
||||
const siteUrl = useSiteUrl();
|
||||
const api = createActivityPubAPI(handle, siteUrl);
|
||||
return useQuery({
|
||||
queryKey: [`followersCount:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowersCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useFollowingCountForUser(handle: string) {
|
||||
const siteUrl = useSiteUrl();
|
||||
const api = createActivityPubAPI(handle, siteUrl);
|
||||
return useQuery({
|
||||
queryKey: [`followingCount:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowingCount();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useFollowingForUser(handle: string) {
|
||||
const siteUrl = useSiteUrl();
|
||||
const api = createActivityPubAPI(handle, siteUrl);
|
||||
return useQuery({
|
||||
queryKey: [`following:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowing();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function useFollowersForUser(handle: string) {
|
||||
const siteUrl = useSiteUrl();
|
||||
const api = createActivityPubAPI(handle, siteUrl);
|
||||
|
||||
return useQuery({
|
||||
queryKey: [`followers:${handle}`],
|
||||
async queryFn() {
|
||||
return api.getFollowers();
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Add table
Reference in a new issue