Skip to content

Commit 734c1cd

Browse files
Added follow buttons to Reader and Notifications (#25561)
ref https://linear.app/ghost/issue/BER-2980/easier-follow-button-in-article-view ref https://linear.app/ghost/issue/BER-2981/follow-button-in-notifications - Added FollowButton component support for link variant - Added follow button in Reader header next to author information - Added follow buttons to notifications (singular follow, group follow expanded view, replies, and mentions)
1 parent 25af482 commit 734c1cd

File tree

5 files changed

+92
-35
lines changed

5 files changed

+92
-35
lines changed

apps/activitypub/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tryghost/activitypub",
3-
"version": "2.0.4",
3+
"version": "2.0.5",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

apps/activitypub/src/api/activitypub.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export interface Notification {
105105
url: string;
106106
handle: string;
107107
avatarUrl: string | null;
108+
followedByMe?: boolean;
108109
},
109110
post: null | {
110111
id: string;

apps/activitypub/src/components/global/FollowButton.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ interface FollowButtonProps {
88
following: boolean;
99
handle: string;
1010
type?: 'primary' | 'secondary';
11+
variant?: 'default' | 'link';
1112
onFollow?: () => void;
1213
onUnfollow?: () => void;
1314
'data-testid'?: string;
@@ -19,6 +20,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
1920
className,
2021
following,
2122
handle,
23+
variant = 'default',
2224
onFollow = noop,
2325
onUnfollow = noop,
2426
'data-testid': testId
@@ -59,6 +61,31 @@ const FollowButton: React.FC<FollowButtonProps> = ({
5961
setIsFollowing(following);
6062
}, [following]);
6163

64+
const buttonText = isFollowing ? 'Following' : 'Follow';
65+
66+
if (variant === 'link') {
67+
return (
68+
<Button
69+
className={clsx(
70+
'p-0 font-medium',
71+
isFollowing
72+
? 'text-gray-700 hover:text-black dark:text-gray-600 dark:hover:text-white'
73+
: 'text-purple hover:text-black dark:hover:text-white',
74+
className
75+
)}
76+
data-testid={testId}
77+
variant="link"
78+
onClick={(event) => {
79+
event?.preventDefault();
80+
event?.stopPropagation();
81+
handleClick();
82+
}}
83+
>
84+
{buttonText}
85+
</Button>
86+
);
87+
}
88+
6289
return (
6390
<Button
6491
className={clsx(
@@ -74,7 +101,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
74101
handleClick();
75102
}}
76103
>
77-
{isFollowing ? 'Following' : 'Follow'}
104+
{buttonText}
78105
</Button>
79106
);
80107
};

apps/activitypub/src/views/Inbox/components/Reader.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import BackButton from '@src/components/global/BackButton';
1313
import DeletedFeedItem from '@src/components/feed/DeletedFeedItem';
1414
import FeedItem from '@src/components/feed/FeedItem';
1515
import FeedItemStats from '@src/components/feed/FeedItemStats';
16+
import FollowButton from '@src/components/global/FollowButton';
1617
import ProfilePreviewHoverCard from '@components/global/ProfilePreviewHoverCard';
1718
import TableOfContents, {TOCItem} from '@src/components/feed/TableOfContents';
1819
import articleBodyStyles from '@src/components/articleBodyStyles';
@@ -807,22 +808,30 @@ export const Reader: React.FC<ReaderProps> = ({
807808
<div className='flex items-center'>
808809
<BackButton className={COLOR_OPTIONS[backgroundColor].button} onClick={onClose} />
809810
</div>
810-
<ProfilePreviewHoverCard actor={actor} isCurrentUser={object.authored}>
811-
<div className='col-[2/3] mx-auto flex w-full cursor-pointer items-center gap-3 max-md:hidden'>
812-
<div className='relative z-10 pt-0.5'>
813-
<APAvatar author={actor}/>
814-
</div>
815-
<div className='relative z-10 mt-0.5 flex w-full min-w-0 cursor-pointer flex-col overflow-visible text-[1.5rem]' onClick={e => handleProfileClick(actor, navigate, e)}>
816-
<div className='flex w-full'>
817-
<span className='min-w-0 truncate whitespace-nowrap font-semibold text-black hover:underline dark:text-white'>{isLoadingContent ? <Skeleton className='w-20' /> : actor.name}</span>
811+
<div className='col-[2/3] mx-auto flex w-full items-center justify-between gap-3 max-md:hidden'>
812+
<ProfilePreviewHoverCard actor={actor} isCurrentUser={object.authored}>
813+
<div className='flex cursor-pointer items-center gap-3'>
814+
<div className='relative z-10 pt-0.5'>
815+
<APAvatar author={actor}/>
818816
</div>
819-
<div className='flex w-full'>
820-
{!isLoadingContent && <span className='truncate text-gray-700 after:mx-1 after:font-normal after:text-gray-700 after:content-["·"]'>{getUsername(actor)}</span>}
821-
<span className='text-gray-700'>{isLoadingContent ? <Skeleton className='w-[120px]' /> : renderTimestamp(object, !object.authored)}</span>
817+
<div className='relative z-10 mt-0.5 flex min-w-0 cursor-pointer flex-col overflow-visible text-[1.5rem]' onClick={e => handleProfileClick(actor, navigate, e)}>
818+
<div className='flex w-full'>
819+
<span className='min-w-0 truncate whitespace-nowrap font-semibold text-black hover:underline dark:text-white'>{isLoadingContent ? <Skeleton className='w-20' /> : actor.name}</span>
820+
</div>
821+
<div className='flex w-full'>
822+
{!isLoadingContent && <span className='truncate text-gray-700 after:mx-1 after:font-normal after:text-gray-700 after:content-["·"]'>{getUsername(actor)}</span>}
823+
<span className='text-gray-700'>{isLoadingContent ? <Skeleton className='w-[120px]' /> : renderTimestamp(object, !object.authored)}</span>
824+
</div>
822825
</div>
823826
</div>
824-
</div>
825-
</ProfilePreviewHoverCard>
827+
</ProfilePreviewHoverCard>
828+
{!object.authored && !isLoadingContent && (
829+
<FollowButton
830+
following={actor.followedByMe ?? false}
831+
handle={getUsername(actor)}
832+
/>
833+
)}
834+
</div>
826835
<div className='col-[3/4] flex items-center justify-end gap-2'>
827836
<Customizer
828837
backgroundColor={backgroundColor}

apps/activitypub/src/views/Notifications/Notifications.tsx

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Button, LoadingIndicator, LucideIcon, Skeleton} from '@tryghost/shade';
55
import APAvatar from '@components/global/APAvatar';
66
import Error from '@components/layout/Error';
77
import FeedItemStats from '@components/feed/FeedItemStats';
8+
import FollowButton from '@components/global/FollowButton';
89
import Layout from '@components/layout';
910
import NotificationIcon from './components/NotificationIcon';
1011
import NotificationItem from './components/NotificationItem';
@@ -118,7 +119,7 @@ const NotificationGroupDescription: React.FC<NotificationGroupDescriptionProps>
118119

119120
const actorText = (
120121
<>
121-
<ProfilePreviewHoverCard actor={firstActor as unknown as ActorProperties} isCurrentUser={false}>
122+
<ProfilePreviewHoverCard actor={firstActor as unknown as ActorProperties} align="center" isCurrentUser={false}>
122123
<span
123124
className={actorClass}
124125
onClick={(e) => {
@@ -329,7 +330,7 @@ const Notifications: React.FC = () => {
329330
)
330331
}
331332
{group.actors.length > 1 && <NotificationItem.Avatars>
332-
<div className='flex flex-col'>
333+
<div className='flex w-full flex-col'>
333334
<div className='relative flex items-center pl-2'>
334335
{!openStates[group.id || `${group.type}_${index}`] && group.actors.slice(0, maxAvatars).map((actor: ActorProperties) => (
335336
<APAvatar
@@ -367,21 +368,30 @@ const Notifications: React.FC = () => {
367368
{group.actors.map((actor: ActorProperties) => (
368369
<div
369370
key={actor.id}
370-
className='flex items-center break-anywhere hover:opacity-80'
371+
className='group/item flex items-center justify-between gap-4 break-anywhere'
371372
onClick={(e) => {
372373
e?.stopPropagation();
373374
handleProfileClick(actor.handle, navigate);
374375
}}
375376
>
376-
<APAvatar author={{
377-
icon: {
378-
url: actor.avatarUrl || ''
379-
},
380-
name: actor.name,
381-
handle: actor.handle
382-
}} size='xs' />
383-
<span className='ml-2 line-clamp-1 text-base font-semibold dark:text-white'>{actor.name}</span>
384-
<span className='ml-1 line-clamp-1 text-base text-gray-700 dark:text-gray-600'>{actor.handle}</span>
377+
<div className='flex min-w-0 items-center'>
378+
<APAvatar author={{
379+
icon: {
380+
url: actor.avatarUrl || ''
381+
},
382+
name: actor.name,
383+
handle: actor.handle
384+
}} size='xs' />
385+
<span className='ml-2 line-clamp-1 text-base font-semibold group-hover/item:underline dark:text-white'>{actor.name}</span>
386+
<span className='ml-1 line-clamp-1 text-base text-gray-700 dark:text-gray-600'>{actor.handle}</span>
387+
</div>
388+
{group.type === 'follow' && !actor.followedByMe && (
389+
<FollowButton
390+
following={false}
391+
handle={actor.handle}
392+
variant="link"
393+
/>
394+
)}
385395
</div>
386396
))}
387397
</div>
@@ -396,14 +406,24 @@ const Notifications: React.FC = () => {
396406
<Skeleton />
397407
<Skeleton className='w-full max-w-60' />
398408
</> :
399-
<div className='flex items-center gap-1'>
400-
<span className='truncate'><NotificationGroupDescription group={group} /></span>
401-
{group.actors.length < 2 &&
402-
<>
403-
<span className='mt-px text-[8px] text-gray-700 dark:text-gray-600'>&bull;</span>
404-
<span className='mt-0.5 text-sm text-gray-700 dark:text-gray-600'>{renderTimestamp(group, false)}</span>
405-
</>
406-
}
409+
<div className='flex justify-between'>
410+
<div className='flex items-center gap-1'>
411+
<span className='truncate'><NotificationGroupDescription group={group} /></span>
412+
{group.actors.length < 2 &&
413+
<>
414+
<span className='mt-px text-[8px] text-gray-700 dark:text-gray-600'>&bull;</span>
415+
<span className='mt-0.5 text-sm text-gray-700 dark:text-gray-600'>{renderTimestamp(group, false)}</span>
416+
</>
417+
}
418+
</div>
419+
{/* Follow button for singular follow, reply, and mention */}
420+
{group.actors.length === 1 && (group.type === 'follow' || group.type === 'reply' || group.type === 'mention') && !group.actors[0].followedByMe && (
421+
<FollowButton
422+
following={false}
423+
handle={group.actors[0].handle}
424+
variant="link"
425+
/>
426+
)}
407427
</div>
408428
}
409429
</div>

0 commit comments

Comments
 (0)