0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Added notification for reposts (#22109)

ref https://linear.app/ghost/issue/AP-695/show-a-notification-when-someone-reposts-your-content

- When someone reposts your post or note, you’ll now receive a notification. If multiple accounts repost the same piece of content, those notifications will be grouped together, but only if they’re fetched as the same page of notifications.
- Converted functions to React components
- Bumped the package
This commit is contained in:
Djordje Vlaisavljevic 2025-02-05 20:13:19 +01:00 committed by GitHub
parent 2e2704427c
commit a7aa59de72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 42 deletions

View file

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

View file

@ -1,5 +1,5 @@
import Activities from './components/Activities';
import Inbox from './components/Inbox';
import Notifications from './components/Notifications';
import Profile from './components/Profile';
import Search from './components/Search';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@ -10,8 +10,8 @@ const MainContent = () => {
switch (mainRoute) {
case 'search':
return <Search />;
case 'activity':
return <Activities />;
case 'notifications':
return <Notifications />;
case 'profile':
return <Profile />;
default:

View file

@ -21,13 +21,14 @@ import {
import {type NotificationType} from './activities/NotificationIcon';
import {handleProfileClick} from '../utils/handle-profile-click';
interface ActivitiesProps {}
interface NotificationsProps {}
// eslint-disable-next-line no-shadow
enum ACTIVITY_TYPE {
CREATE = 'Create',
LIKE = 'Like',
FOLLOW = 'Follow'
FOLLOW = 'Follow',
REPOST = 'Announce'
}
interface GroupedActivity {
@ -37,26 +38,9 @@ interface GroupedActivity {
id?: string;
}
const getExtendedDescription = (activity: GroupedActivity): JSX.Element | null => {
// If the activity is a reply
if (Boolean(activity.type === ACTIVITY_TYPE.CREATE && activity.object?.inReplyTo)) {
return (
<div
dangerouslySetInnerHTML={{__html: stripHtml(activity.object?.content || '')}}
className='ap-note-content mt-1 line-clamp-2 text-pretty text-grey-700'
/>
);
} else if (activity.type === ACTIVITY_TYPE.LIKE && !activity.object?.name && activity.object?.content) {
return (
<div
dangerouslySetInnerHTML={{__html: stripHtml(activity.object?.content || '')}}
className='ap-note-content mt-1 line-clamp-2 text-pretty text-grey-700'
></div>
);
}
return null;
};
interface NotificationGroupDescriptionProps {
group: GroupedActivity;
}
const getActivityBadge = (activity: GroupedActivity): NotificationType => {
switch (activity.type) {
@ -65,12 +49,10 @@ const getActivityBadge = (activity: GroupedActivity): NotificationType => {
case ACTIVITY_TYPE.FOLLOW:
return 'follow';
case ACTIVITY_TYPE.LIKE:
if (activity.object) {
return 'like';
}
return 'like';
case ACTIVITY_TYPE.REPOST:
return 'repost';
}
return 'like';
};
const groupActivities = (activities: Activity[]): GroupedActivity[] => {
@ -91,6 +73,12 @@ const groupActivities = (activities: Activity[]): GroupedActivity[] => {
groupKey = `like_${activity.object.id}`;
}
break;
case ACTIVITY_TYPE.REPOST:
if (activity.object?.id) {
// Group reposts by the target object
groupKey = `announce_${activity.object.id}`;
}
break;
case ACTIVITY_TYPE.CREATE:
// Don't group creates/replies
groupKey = `create_${activity.id}`;
@ -116,7 +104,7 @@ const groupActivities = (activities: Activity[]): GroupedActivity[] => {
return Object.values(groups);
};
const getGroupDescription = (group: GroupedActivity): JSX.Element => {
const NotificationGroupDescription: React.FC<NotificationGroupDescriptionProps> = ({group}) => {
const [firstActor, secondActor, ...otherActors] = group.actors;
const hasOthers = otherActors.length > 0;
@ -145,7 +133,9 @@ const getGroupDescription = (group: GroupedActivity): JSX.Element => {
case ACTIVITY_TYPE.FOLLOW:
return <>{actorText} started following you</>;
case ACTIVITY_TYPE.LIKE:
return <>{actorText} liked your post <span className='font-semibold'>{group.object?.name || ''}</span></>;
return <>{actorText} liked your {group.object?.type === 'Article' ? 'post' : 'note'} <span className='font-semibold'>{group.object?.name || ''}</span></>;
case ACTIVITY_TYPE.REPOST:
return <>{actorText} reposted your {group.object?.type === 'Article' ? 'post' : 'note'} <span className='font-semibold'>{group.object?.name || ''}</span></>;
case ACTIVITY_TYPE.CREATE:
if (group.object?.inReplyTo && typeof group.object?.inReplyTo !== 'string') {
let content = stripHtml(group.object.inReplyTo.content || '');
@ -162,7 +152,7 @@ const getGroupDescription = (group: GroupedActivity): JSX.Element => {
return <></>;
};
const Activities: React.FC<ActivitiesProps> = ({}) => {
const Notifications: React.FC<NotificationsProps> = () => {
const user = 'index';
const [openStates, setOpenStates] = React.useState<{[key: string]: boolean}>({});
@ -183,7 +173,7 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
includeOwn: true,
includeReplies: true,
filter: {
type: ['Follow', 'Like', `Create:Note`]
type: ['Follow', 'Like', `Create:Note`, `Announce:Note`, `Announce:Article`]
},
limit: 120,
key: GET_ACTIVITIES_QUERY_KEY_NOTIFICATIONS
@ -215,6 +205,14 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
return true;
})
// Remove reposts that are not for our own posts
.filter((activity) => {
if (activity.type === ACTIVITY_TYPE.REPOST && activity.object?.attributedTo?.id !== userProfile?.id) {
return false;
}
return true;
})
// Remove create activities that are not replies to our own posts
.filter((activity) => {
if (
@ -292,6 +290,14 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
handleProfileClick(group.actors[0]);
}
break;
case ACTIVITY_TYPE.REPOST:
NiceModal.show(ArticleModal, {
activityId: group.id,
object: group.object,
actor: group.object.attributedTo as ActorProperties,
width: group.object?.type === 'Article' ? 'wide' : 'narrow'
});
break;
}
};
@ -377,9 +383,18 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
</NotificationItem.Avatars>
<NotificationItem.Content>
<div className='line-clamp-2 text-pretty text-black'>
{getGroupDescription(group)}
<NotificationGroupDescription group={group} />
</div>
{getExtendedDescription(group)}
{(
(group.type === ACTIVITY_TYPE.CREATE && group.object?.inReplyTo) ||
(group.type === ACTIVITY_TYPE.LIKE && !group.object?.name && group.object?.content) ||
(group.type === ACTIVITY_TYPE.REPOST && !group.object?.name && group.object?.content)
) && (
<div
dangerouslySetInnerHTML={{__html: stripHtml(group.object?.content || '')}}
className='ap-note-content mt-1 line-clamp-2 text-pretty text-grey-700'
/>
)}
</NotificationItem.Content>
</NotificationItem>
{index < groupedActivities.length - 1 && <Separator />}
@ -400,4 +415,4 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
);
};
export default Activities;
export default Notifications;

View file

@ -1,7 +1,7 @@
import React from 'react';
import {Icon} from '@tryghost/admin-x-design-system';
export type NotificationType = 'like' | 'follow' | 'reply';
export type NotificationType = 'like' | 'follow' | 'reply' | 'repost';
interface NotificationIconProps {
notificationType: NotificationType;
@ -29,6 +29,11 @@ const NotificationIcon: React.FC<NotificationIconProps> = ({notificationType, cl
iconColor = 'text-purple-500';
badgeColor = 'bg-purple-100/50';
break;
case 'repost':
icon = 'reload';
iconColor = 'text-green-500';
badgeColor = 'bg-green-100/50';
break;
}
return (

View file

@ -25,9 +25,24 @@ const MainNavigation: React.FC<MainNavigationProps> = ({page}) => {
unstyled
onClick={() => updateRoute('feed')}
/>
<Button className={`${page === 'activities' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Notifications' unstyled onClick={() => updateRoute('activity')} />
<Button className={`${page === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Search' unstyled onClick={() => updateRoute('search')} />
<Button className={`${page === 'profile' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Profile' unstyled onClick={() => updateRoute('profile')} />
<Button
className={`${page === 'notifications' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Notifications'
unstyled
onClick={() => updateRoute('notifications')}
/>
<Button
className={`${page === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Search'
unstyled
onClick={() => updateRoute('search')}
/>
<Button
className={`${page === 'profile' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Profile'
unstyled
onClick={() => updateRoute('profile')}
/>
</div>
</MainHeader>
);