0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Updated ActivityPub design (#21327)

ref https://linear.app/ghost/issue/AP-476/remove-static-buttons-from-notifications-and-resolve-css-issues, https://linear.app/ghost/issue/AP-449/remove-follow-button-and-component, https://linear.app/ghost/issue/AP-448/add-loading-state-for-home-tab, https://linear.app/ghost/issue/AP-446/update-top-nav-bar-design

- Added the Suggestions sidebar
- Added real data to `Your profile` tab
- Switched navigation in top-bar to text-based
- Added loading states to Home and Activity tabs
- Fixed overflow and z-index CSS issues
- Removed `Unfollow` modal since now have a more user-friendly way to follow users
- Changed link color to blue
This commit is contained in:
Djordje Vlaisavljevic 2024-10-21 20:24:36 +01:00 committed by GitHub
parent 4277c2a9d5
commit 7c32527159
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 309 additions and 241 deletions

View file

@ -6,12 +6,12 @@ import {LoadingIndicator, NoValueLabel} from '@tryghost/admin-x-design-system';
import APAvatar, {AvatarBadge} from './global/APAvatar';
import ActivityItem, {type Activity} from './activities/ActivityItem';
import ArticleModal from './feed/ArticleModal';
import FollowButton from './global/FollowButton';
// import FollowButton from './global/FollowButton';
import MainNavigation from './navigation/MainNavigation';
import getUsername from '../utils/get-username';
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
import {useFollowersForUser} from '../MainContent';
// import {useFollowersForUser} from '../MainContent';
interface ActivitiesProps {}
@ -91,7 +91,8 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
isFetchingNextPage,
isLoading
} = useActivitiesForUser({
handle: user,
includeOwn: true,
@ -129,66 +130,75 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
// Retrieve followers for the user
const {data: followers = []} = useFollowersForUser(user);
// const {data: followers = []} = useFollowersForUser(user);
const isFollower = (id: string): boolean => {
return followers.includes(id);
};
// const isFollower = (id: string): boolean => {
// return followers.includes(id);
// };
return (
<>
<MainNavigation title='Activities' />
<div className='z-0 flex w-full flex-col items-center'>
{activities.length === 0 && (
<div className='mt-8'>
<NoValueLabel icon='bell'>
When other Fediverse users interact with you, you&apos;ll see it here.
</NoValueLabel>
</div>
)}
{activities.length > 0 && (
<>
<div className='mt-8 flex w-full max-w-[560px] flex-col'>
{activities?.map(activity => (
<ActivityItem
key={activity.id}
url={getActivityUrl(activity) || getActorUrl(activity)}
onClick={
activity.type === ACTVITY_TYPE.CREATE ? () => {
NiceModal.show(ArticleModal, {
object: activity.object,
actor: activity.actor,
comments: activity.object.replies
});
} : undefined
}
>
<APAvatar author={activity.actor} badge={getActivityBadge(activity)} />
<div className='pt-[2px]'>
<div className='text-grey-600'>
<span className='mr-1 font-bold text-black'>{activity.actor.name}</span>
{getUsername(activity.actor)}
</div>
<div className=''>{getActivityDescription(activity)}</div>
{getExtendedDescription(activity)}
</div>
<FollowButton
className='ml-auto'
following={isFollower(activity.actor.id)}
handle={getUsername(activity.actor)}
type='link'
/>
</ActivityItem>
))}
{
isLoading && (<div className='mt-8 flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='lg' />
</div>)
}
{
isLoading === false && activities.length === 0 && (
<div className='mt-8'>
<NoValueLabel icon='bell'>
When other Fediverse users interact with you, you&apos;ll see it here.
</NoValueLabel>
</div>
<div ref={loadMoreRef} className='h-1'></div>
{isFetchingNextPage && (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
)
}
{
(isLoading === false && activities.length > 0) && (
<>
<div className='mt-8 flex w-full max-w-[560px] flex-col'>
{activities?.map(activity => (
<ActivityItem
key={activity.id}
url={getActivityUrl(activity) || getActorUrl(activity)}
onClick={
activity.type === ACTVITY_TYPE.CREATE ? () => {
NiceModal.show(ArticleModal, {
object: activity.object,
actor: activity.actor,
comments: activity.object.replies
});
} : undefined
}
>
<APAvatar author={activity.actor} badge={getActivityBadge(activity)} />
<div className='min-w-0'>
<div className='truncate text-grey-600'>
<span className='mr-1 font-bold text-black'>{activity.actor.name}</span>
{getUsername(activity.actor)}
</div>
<div className=''>{getActivityDescription(activity)}</div>
{getExtendedDescription(activity)}
</div>
{/* <FollowButton
className='ml-auto'
following={isFollower(activity.actor.id)}
handle={getUsername(activity.actor)}
type='link'
/> */}
</ActivityItem>
))}
</div>
)}
</>
)}
<div ref={loadMoreRef} className='h-1'></div>
{isFetchingNextPage && (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
</div>
)}
</>
)
}
</div>
</>
);

View file

@ -1,13 +1,16 @@
import APAvatar from './global/APAvatar';
import ActivityItem, {type Activity} from './activities/ActivityItem';
import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
import ArticleModal from './feed/ArticleModal';
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 {type Activity} from './activities/ActivityItem';
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';
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
import {useActivitiesForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries';
import {useRouting} from '@tryghost/admin-x-framework/routing';
interface InboxProps {}
@ -20,7 +23,8 @@ const Inbox: React.FC<InboxProps> = ({}) => {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
isFetchingNextPage,
isLoading
} = useActivitiesForUser({
handle: 'index',
includeReplies: true,
@ -29,6 +33,11 @@ const Inbox: React.FC<InboxProps> = ({}) => {
}
});
const {updateRoute} = useRouting();
const {suggestedProfilesQuery} = useSuggestedProfiles('index', ['@quillmatiq@mastodon.social', '@miaq@flipboard.social', '@mallory@techpolicy.social']);
const {data: suggested = [], isLoading: isLoadingSuggested} = suggestedProfilesQuery;
const activities = (data?.pages.flatMap(page => page.data) ?? []);
const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[], focusReply = false) => {
@ -96,45 +105,88 @@ const Inbox: React.FC<InboxProps> = ({}) => {
<>
<MainNavigation page='home' title="Home" onLayoutChange={handleLayoutChange} />
<div className='z-0 my-5 flex w-full flex-col'>
<div className='w-full'>
{activities.length > 0 ? (
<div className='w-full px-8'>
{isLoading ? (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='lg' />
</div>
) : activities.length > 0 ? (
<>
<ul className='mx-auto flex max-w-[640px] flex-col'>
{activities.map((activity, index) => (
<li
key={activity.id}
data-test-view-article
onClick={() => handleViewContent(
activity.object,
getContentAuthor(activity),
activity.object.replies
<div className={`mx-auto flex items-start ${layout === 'inbox' ? 'max-w-6xl gap-14' : 'gap-8'}`}>
<div className='flex w-full min-w-0 items-start'>
<ul className={`mx-auto flex ${layout === 'inbox' ? 'max-w-full' : 'max-w-[500px]'} flex-col`}>
{activities.map((activity, index) => (
<li
key={activity.id}
data-test-view-article
onClick={() => handleViewContent(
activity.object,
getContentAuthor(activity),
activity.object.replies
)}
>
<FeedItem
actor={activity.actor}
comments={activity.object.replies}
layout={layout}
object={activity.object}
type={activity.type}
onCommentClick={() => handleViewContent(
activity.object,
getContentAuthor(activity),
activity.object.replies,
true
)}
/>
{index < activities.length - 1 && (
<div className="h-px w-full bg-grey-200"></div>
)}
</li>
))}
<div ref={loadMoreRef} className='h-1'></div>
{isFetchingNextPage && (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
</div>
)}
>
<FeedItem
actor={activity.actor}
comments={activity.object.replies}
layout={layout}
object={activity.object}
type={activity.type}
onCommentClick={() => handleViewContent(
activity.object,
getContentAuthor(activity),
activity.object.replies,
true
)}
/>
{index < activities.length - 1 && (
<div className="h-px w-full bg-grey-200"></div>
)}
</li>
))}
</ul>
<div ref={loadMoreRef} className='h-1'></div>
{isFetchingNextPage && (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
</ul>
</div>
)}
<div className={`sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden ${layout === 'inbox' ? '' : ' xxl:fixed xxl:right-[40px]'}`}>
<h2 className='mb-2 text-lg font-semibold'>You might also like...</h2>
{isLoadingSuggested ? (
<LoadingIndicator size="sm" />
) : (
<ul className='grow'>
{suggested.map((profile) => {
const actor = profile.actor;
// const isFollowing = profile.isFollowing;
return (
<li key={actor.id}>
<ActivityItem url={actor.url}>
<APAvatar author={actor} />
<div>
<div className='text-grey-600'>
<span className='mr-1 truncate font-bold text-black'>{actor.name || actor.preferredUsername || 'Unknown'}</span>
<div className='truncate text-sm'>{getUsername(actor)}</div>
</div>
</div>
{/* <FollowButton
className='ml-auto'
following={isFollowing}
handle={getUsername(actor)}
type='link'
onFollow={() => updateSuggestedProfile(actor.id!, {isFollowing: true})}
onUnfollow={() => updateSuggestedProfile(actor.id!, {isFollowing: false})}
/> */}
</ActivityItem>
</li>
);
})}
</ul>
)}
<Button className='mt-4' color='grey' fullWidth={true} label='Explore' onClick={() => updateRoute('search')} />
</div>
</div>
</>
) : (
<div className='flex items-center justify-center text-center'>
@ -148,7 +200,7 @@ const Inbox: React.FC<InboxProps> = ({}) => {
Welcome to ActivityPub
</Heading>
<p className='text-pretty text-grey-800'>
Were so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.
We&apos;re so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.
</p>
<p className='text-pretty text-grey-800'>
You can see all of the users on the rightfind your favorite ones and give them a follow.

View file

@ -2,10 +2,18 @@ import APAvatar from './global/APAvatar';
import ActivityItem from './activities/ActivityItem';
import FeedItem from './feed/FeedItem';
import MainNavigation from './navigation/MainNavigation';
import React, {useState} from 'react';
import React, {useEffect, useRef, useState} from 'react';
import getUsername from '../utils/get-username';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Heading, List, NoValueLabel, Tab, TabView} from '@tryghost/admin-x-design-system';
import {useFollowersCountForUser, useFollowersForUser, useFollowingCountForUser, useFollowingForUser, useLikedForUser} from '../hooks/useActivityPubQueries';
import {
useFollowersCountForUser,
useFollowersForUser,
useFollowingCountForUser,
useFollowingForUser,
useLikedForUser,
useUserDataForUser
} from '../hooks/useActivityPubQueries';
interface ProfileProps {}
@ -15,6 +23,8 @@ const Profile: React.FC<ProfileProps> = ({}) => {
const {data: following = []} = useFollowingForUser('index');
const {data: followers = []} = useFollowersForUser('index');
const {data: liked = []} = useLikedForUser('index');
// Replace 'index' with the actual handle of the user
const {data: userProfile} = useUserDataForUser('index') as {data: ActorProperties | null};
type ProfileTab = 'posts' | 'likes' | 'following' | 'followers';
@ -131,25 +141,75 @@ const Profile: React.FC<ProfileProps> = ({}) => {
}
].filter(Boolean) as Tab<ProfileTab>[];
const attachments = (userProfile?.attachment || []);
const [isExpanded, setisExpanded] = useState(false);
const toggleExpand = () => {
setisExpanded(!isExpanded);
};
const contentRef = useRef<HTMLDivElement | null>(null);
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
if (contentRef.current) {
setIsOverflowing(contentRef.current.scrollHeight > 160); // Compare content height to max height
}
}, [isExpanded]);
return (
<>
<MainNavigation title='Profile' />
<div className='z-0 flex w-full flex-col items-center'>
<div className='mx-auto mt-8 w-full max-w-[560px]' id='ap-sidebar'>
<div className='h-[200px] w-full rounded-lg bg-gradient-to-tr from-grey-200 to-grey-100'>
</div>
<div className='-mt-8 px-4'>
<div className='inline-flex rounded-lg border-4 border-white'>
<APAvatar size='lg' />
<div className='z-0 mx-auto mt-8 flex w-full max-w-[580px] flex-col items-center pb-16'>
<div className='mx-auto w-full'>
{userProfile?.image && (<div className='h-[200px] w-full overflow-hidden rounded-lg bg-gradient-to-tr from-grey-200 to-grey-100'>
<img
alt={userProfile?.name}
className='h-full w-full object-cover'
src={userProfile?.image.url}
/>
</div>)}
<div className={`${userProfile?.image && '-mt-12'} px-4`}>
<div className='flex items-end justify-between'>
<div className='rounded-xl outline outline-4 outline-white'>
<APAvatar
author={userProfile as ActorProperties}
size='lg'
/>
</div>
</div>
<Heading className='mt-4' level={3}>Building ActivityPub</Heading>
<span className='mt-1 text-[1.5rem] text-grey-800'>@index@activitypub.ghost.org</span>
<p className='mt-3 text-[1.5rem]'>Ghost is federating over ActivityPub to become part of the world&apos;s largest publishing network</p>
<span className='mt-3 line-clamp-1 flex'>
<span className={`mr-1 after:content-[":"]`}>Website</span>
<a className='truncate text-[1.5rem] underline' href='https://activitypub.ghost.org'>activitypub.ghost.org</a>
<Heading className='mt-4' level={3}>{userProfile?.name}</Heading>
<span className='mt-1 text-[1.5rem] text-grey-800'>
<span>{userProfile && getUsername(userProfile)}</span>
</span>
<TabView<'posts' | 'likes' | 'following' | 'followers'> containerClassName='mt-6' selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
{(userProfile?.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: userProfile?.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>

View file

@ -86,7 +86,7 @@ const Search: React.FC<SearchProps> = ({}) => {
const results = data?.profiles || [];
const showLoading = (isFetching || isQuerying) && !isFetched;
const showNoResults = isFetched && results.length === 0;
const showNoResults = isFetched && results.length === 0 && (query.length > 0);
const showSuggested = query === '' || (isFetched && results.length === 0);
useEffect(() => {

View file

@ -26,7 +26,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({children, url = null, onClic
onClick();
}
}}>
<div className='flex w-full gap-4 border-b border-grey-100 px-2 py-4'>
<div className='flex w-full gap-3 border-b border-grey-100 px-2 py-4'>
{childrenArray[0]}
{childrenArray[1]}
{childrenArray[2]}

View file

@ -146,7 +146,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({object, actor, comments, foc
</div>
)}
<div className='col-[2/3] flex grow items-center justify-center px-8 text-center'>
{/* <span className='text-lg font-semibold text-grey-900'>{object.type}</span> */}
</div>
<div className='col-[3/4] flex items-center justify-end space-x-6 px-8'>
<Button icon='angle-brackets' size='md' unstyled onClick={toggleModalSize}/>

View file

@ -6,6 +6,7 @@ import APAvatar from '../global/APAvatar';
import getRelativeTimestamp from '../../utils/get-relative-timestamp';
import getUsername from '../../utils/get-username';
import stripHtml from '../../utils/strip-html';
import {type Activity} from '../activities/ActivityItem';
import {useLikeMutationForUser, useUnlikeMutationForUser} from '../../hooks/useActivityPubQueries';
@ -140,8 +141,8 @@ function renderInboxAttachment(object: ObjectProperties) {
);
default:
if (object.image) {
return <div className='min-w-[160px]'>
<img className={`h-[100px] w-[160px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={object.image} />
return <div className='min-h-[80px]'>
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={object.image} />
</div>;
}
return null;
@ -169,7 +170,8 @@ const FeedItemStats: React.FC<{
const likeMutation = useLikeMutationForUser('index');
const unlikeMutation = useUnlikeMutationForUser('index');
const handleLikeClick = async () => {
const handleLikeClick = async (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
setIsClicked(true);
if (!isLiked) {
likeMutation.mutate(object.id);
@ -194,7 +196,9 @@ const FeedItemStats: React.FC<{
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
handleLikeClick();
if (e) {
handleLikeClick(e);
}
}}
/>
{isLiked && (layout !== 'inbox') && <span className={`text-grey-900`}>{new Intl.NumberFormat().format(likeCount)}</span>}
@ -250,7 +254,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
};
const handleCopyLink = async () => {
if (object?.url) { // Check if url is defined
if (object?.url) {
await navigator.clipboard.writeText(object.url);
setIsCopied(true);
showToast({
@ -284,7 +288,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
const UserMenuTrigger = (
<Button
className={`relative z-10 ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
className={`relative z-[9998] ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
hideLabel={true}
icon='dotdotdot'
iconColorClass={`(${layout === 'inbox' ? 'text-grey-900' : 'text-grey-600'}`}
@ -303,20 +307,20 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<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 pb-6`} data-test-activity>
<div className={`border-1 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-x-3 gap-y-2 pb-6`} data-test-activity>
<APAvatar author={author}/>
<div className='flex justify-between'>
<div className='relative z-10 flex w-full flex-col overflow-visible text-[1.5rem]'>
<div className='flex min-w-0 justify-between'>
<div className='relative z-10 flex w-full flex-col overflow-visible text-md'>
<div className='flex justify-between'>
<div className='flex'>
<span className='truncate whitespace-nowrap font-bold' data-test-activity-heading>{author.name}</span>
<div className='flex w-full'>
<span className='min-w-0 truncate break-all font-semibold' data-test-activity-heading>{author.name}</span>
<span className='ml-1 truncate text-grey-700'>{getUsername(author)}</span>
</div>
{renderTimestamp(object)}
<div className='ml-2'>{renderTimestamp(object)}</div>
</div>
</div>
</div>
<div className={`relative z-10 col-start-2 col-end-3 w-full gap-4`}>
<div className={`relative col-start-2 col-end-3 w-full gap-4`}>
<div className='flex flex-col'>
<div className='mt-[-24px]'>
{(object.type === 'Article') && renderFeedAttachment(object, layout)}
@ -332,7 +336,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
size='md'
/>}
</div>
<div className='space-between mt-5 flex'>
<div className='space-between relative z-[30] mt-5 flex'>
<FeedItemStats
commentCount={comments.length}
layout={layout}
@ -345,9 +349,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
</div>
</div>
</div>
{/* </div> */}
</div>
{/* <div className={`absolute -inset-x-3 -inset-y-0 z-0 rounded transition-colors ${(layout === 'feed') ? 'group-hover/article:bg-grey-75' : ''} `}></div> */}
</div>
)}
</>
@ -357,7 +359,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<>
{object && (
<div>
<div className={`group/article relative cursor-pointer`} onClick={onClick}>
<div className={`group/article relative`} onClick={onClick}>
{(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>
@ -366,14 +368,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<div className='relative z-10 pt-[3px]'>
<APAvatar author={author}/>
</div>
{/* <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'>
<span className='truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
{renderTimestamp(object)}
<div className='relative z-10 flex w-full min-w-0 flex-col overflow-visible text-[1.5rem]'>
<div className='flex w-full'>
<span className='min-w-0 truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
<div className='ml-2'>{renderTimestamp(object)}</div>
</div>
<div className='flex'>
<span className='truncate text-grey-700'>{getUsername(author)}</span>
<div className='flex w-full'>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
</div>
</div>
<div className={`relative z-10 col-start-1 col-end-3 w-full gap-4`}>
@ -394,7 +395,6 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
</div>
</div>
</div>
{/* </div> */}
</div>
<div className={`absolute -inset-x-3 -inset-y-0 z-0 rounded transition-colors`}></div>
</div>
@ -414,13 +414,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<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`} data-test-activity>
<div className='relative z-10 pt-[3px]'>
<div className='relative z-10 min-w-0 pt-[3px]'>
<APAvatar author={author}/>
</div>
<div className='relative z-10 flex w-full flex-col overflow-visible text-[1.5rem]'>
<div className='relative z-10 flex w-full min-w-0 flex-col overflow-visible text-[1.5rem]'>
<div className='flex'>
<span className='truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
{renderTimestamp(object)}
<span className='min-w-0 truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
<div className='ml-2'>{renderTimestamp(object)}</div>
</div>
<div className='flex'>
<span className='truncate text-grey-700'>{getUsername(author)}</span>
@ -455,23 +455,31 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
return (
<>
{object && (
<div className='group/article relative -mx-4 -mt-px flex cursor-pointer justify-between rounded-md p-4 hover:bg-grey-75' onClick={onClick}>
<div className='flex w-full flex-col items-start justify-between gap-1 pr-4'>
<div className='z-10 flex items-start justify-between gap-2 group-hover/article:border-transparent'>
<div className='group/article relative -mx-4 -my-px flex min-w-0 cursor-pointer justify-between rounded-md p-4 hover:bg-grey-75' onClick={onClick}>
<div className='flex w-full min-w-0 flex-col items-start justify-between gap-1 pr-4'>
<div className='z-10 flex w-full min-w-0 items-start gap-2 group-hover/article:border-transparent'>
<APAvatar author={author} size='xs'/>
<div className='z-10 w-full text-sm'>
<div>
<span className='truncate whitespace-nowrap font-semibold' data-test-activity-heading>{author.name}</span>
<span className='truncate text-grey-700'>&nbsp;{getUsername(author)}</span>
<span className='whitespace-nowrap text-grey-700 before:mx-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
</div>
</div>
<span className='min-w-0 truncate break-all font-semibold' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
{/* <div className='flex gap-2'>
<span className='truncate min-w-0 break-all font-semibold' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
</div> */}
<span className='shrink-0 whitespace-nowrap text-grey-700 before:mr-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
</div>
<Heading className='line-clamp-1 font-semibold leading-normal' level={5} data-test-activity-heading>{object.name ? object.name : <span dangerouslySetInnerHTML={({__html: object.content})}></span>}</Heading>
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content line-clamp-1 text-pretty text-[1.5rem] text-grey-700'></div>
<Heading className='line-clamp-1 font-semibold leading-normal' level={5} data-test-activity-heading>
{object.name ? object.name : (
<span dangerouslySetInnerHTML={{
__html: object.content.length > 30
? stripHtml(object.content).substring(0, 50) + '...'
: stripHtml(object.content)
}}></span>
)}
</Heading>
<div dangerouslySetInnerHTML={({__html: stripHtml(object.content)})} className='ap-note-content w-full truncate text-[1.5rem] text-grey-700'></div>
</div>
{renderInboxAttachment(object)}
<div className='invisible absolute right-2 top-[9px] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
<div className='invisible absolute right-2 top-[9px] z-[9998] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
<FeedItemStats
commentCount={comments.length}
layout={layout}

View file

@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React, {useEffect, useState} from 'react';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Icon} from '@tryghost/admin-x-design-system';
@ -15,10 +15,14 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
let iconSize = 18;
let containerClass = '';
let imageClass = 'z-10 rounded w-10 h-10 object-cover';
const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-2 border-2 border-white content-box flex items-center justify-center `;
const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-[0.6rem] border-2 border-white content-box flex items-center justify-center `;
let badgeColor = '';
const [iconUrl, setIconUrl] = useState(author?.icon?.url);
useEffect(() => {
setIconUrl(author?.icon?.url);
}, [author?.icon?.url]);
switch (badge) {
case 'user-fill':
badgeColor = ' bg-blue-500';

View file

@ -8,7 +8,6 @@ import {Activity} from '../activities/ActivityItem';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, showToast} from '@tryghost/admin-x-design-system';
import {useReplyMutationForUser, useUserDataForUser} from '../../hooks/useActivityPubQueries';
// import {useFocusContext} from '@tryghost/admin-x-design-system/types/providers/DesignSystemProvider';
export interface APTextAreaProps extends HTMLProps<HTMLTextAreaElement> {
title?: string;
@ -35,9 +34,6 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
object,
focused,
onNewReply,
// onChange,
// onFocus,
// onBlur,
...props
}) => {
const id = useId();
@ -131,8 +127,8 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
{hint}
</div>
</FormPrimitive.Root>
<div className='absolute bottom-[6px] right-[9px] flex space-x-4 transition-[opacity] duration-150'>
<Button color='black' disabled={buttonDisabled} id='post' label='Post' loading={replyMutation.isLoading} size='sm' onMouseDown={handleClick} />
<div className='absolute bottom-[3px] right-[9px] flex space-x-4 transition-[opacity] duration-150'>
<Button color='black' disabled={buttonDisabled} id='post' label='Post' loading={replyMutation.isLoading} size='md' onMouseDown={handleClick} />
</div>
</div>
</div>

View file

@ -1,57 +0,0 @@
import {useState} from 'react';
import NiceModal from '@ebay/nice-modal-react';
import {Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
import {useRouting} from '@tryghost/admin-x-framework/routing';
import {useFollow} from '../../hooks/useActivityPubQueries';
const FollowSite = NiceModal.create(() => {
const {updateRoute} = useRouting();
const modal = NiceModal.useModal();
const [profileName, setProfileName] = useState('');
const [errorMessage, setError] = useState(null);
async function onSuccess() {
showToast({
message: 'Site followed',
type: 'success'
});
modal.remove();
updateRoute('');
}
async function onError() {
setError(errorMessage);
}
const mutation = useFollow('index', onSuccess, onError);
return (
<Modal
afterClose={() => {
mutation.reset();
updateRoute('');
}}
cancelLabel='Cancel'
okLabel='Follow'
size='sm'
title='Follow a Ghost site'
onOk={() => mutation.mutate(profileName)}
>
<div className='mt-3 flex flex-col gap-4'>
<TextField
autoFocus={true}
error={Boolean(errorMessage)}
hint={errorMessage}
placeholder='@username@hostname'
title='Profile name'
value={profileName}
data-test-new-follower
onChange={e => setProfileName(e.target.value)}
/>
</div>
</Modal>
);
});
export default FollowSite;

View file

@ -1,10 +1,9 @@
import FollowSite from './inbox/FollowSiteModal';
import ViewFollowers from './profile/ViewFollowersModal';
import ViewFollowing from './profile/ViewFollowingModal';
import {ModalComponent} from '@tryghost/admin-x-framework/routing';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const modals = {FollowSite, ViewFollowing, ViewFollowers} satisfies {[key: string]: ModalComponent<any>};
const modals = {ViewFollowing, ViewFollowers} satisfies {[key: string]: ModalComponent<any>};
export default modals;

View file

@ -10,7 +10,6 @@ interface MainNavigationProps {
}
const MainNavigation: React.FC<MainNavigationProps> = ({
title = 'Home',
page = '',
onLayoutChange
}) => {
@ -26,16 +25,11 @@ const MainNavigation: React.FC<MainNavigationProps> = ({
return (
<MainHeader>
<div className='col-[1/2] px-8'>
<h2 className='mt-1 text-xl font-bold'>
{title}
</h2>
</div>
<div className='col-[2/3] flex items-center justify-center gap-9'>
<Button icon='home' iconColorClass={mainRoute === '' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('')} />
<Button icon='magnifying-glass' iconColorClass={mainRoute === 'search' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('search')} />
<Button icon='bell' iconColorClass={mainRoute === 'activity' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('activity')} />
<Button icon='user' iconColorClass={mainRoute === 'profile' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('profile')} />
<div className='col-[1/2] flex gap-8 px-8'>
<Button className={` ${mainRoute === '' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Inbox' unstyled onClick={() => updateRoute('')} />
<Button className={` ${mainRoute === 'activity' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Notifications' unstyled onClick={() => updateRoute('activity')} />
<Button className={` ${mainRoute === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Search' unstyled onClick={() => updateRoute('search')} />
<Button className={` ${mainRoute === 'profile' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Profile' unstyled onClick={() => updateRoute('profile')} />
</div>
<div className='col-[3/4] flex items-center justify-end gap-2 px-8'>
{page === 'home' &&
@ -52,9 +46,6 @@ const MainNavigation: React.FC<MainNavigationProps> = ({
</Tooltip>
</div>
}
<Button color='black' icon='add' label="Follow" onClick={() => {
updateRoute('follow-site');
}} />
</div>
</MainHeader>
);

View file

@ -25,12 +25,14 @@ animation: bump 0.3s ease-in-out;
}
.ap-note-content a, .ap-profile-content a {
color: rgb(236 72 153) !important;
/* color: rgb(236 72 153) !important; */
color: rgb(37 99 235) !important;
word-break: break-all;
}
.ap-note-content a:hover, .ap-profile-content a:hover {
color: rgb(190, 25, 99) !important;
/* color: rgb(190, 25, 99) !important; */
color: rgb(29 78 216) !important;
text-decoration: underline !important;
}

View file

@ -0,0 +1,3 @@
export default function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, '');
}

View file

@ -27,11 +27,11 @@ const Popover: React.FC<PopoverProps> = ({
return (
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
<PopoverPrimitive.Anchor asChild>
<PopoverPrimitive.Trigger asChild>
<PopoverPrimitive.Trigger asChild onClick={e => e.stopPropagation()}>
{trigger}
</PopoverPrimitive.Trigger>
</PopoverPrimitive.Anchor>
<PopoverPrimitive.Content align={position} className="z-50 mt-2 origin-top-right rounded bg-white shadow-md ring-1 ring-[rgba(0,0,0,0.01)] focus:outline-none dark:bg-grey-900 dark:text-white" data-testid='popover-content' side="bottom" onClick={handleContentClick}>
<PopoverPrimitive.Content align={position} className="z-[9999] mt-2 origin-top-right rounded bg-white shadow-md ring-1 ring-[rgba(0,0,0,0.01)] focus:outline-none dark:bg-grey-900 dark:text-white" data-testid='popover-content' side="bottom" onClick={handleContentClick}>
{children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Root>

View file

@ -13,6 +13,7 @@ module.exports = {
md: '640px',
lg: '1024px',
xl: '1320px',
xxl: '1440px',
tablet: '860px'
},
colors: {