Skip to content

Commit

Permalink
Added notification for reposts (#22109)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
djordjevlais authored Feb 5, 2025
1 parent 2e27044 commit a7aa59d
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 42 deletions.
2 changes: 1 addition & 1 deletion apps/admin-x-activitypub/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/admin-x-activitypub",
"version": "0.3.58",
"version": "0.3.59",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
6 changes: 3 additions & 3 deletions apps/admin-x-activitypub/src/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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[] => {
Expand All @@ -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}`;
Expand All @@ -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;

Expand Down Expand Up @@ -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 || '');
Expand All @@ -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}>({});
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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;
}
};

Expand Down Expand Up @@ -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 />}
Expand All @@ -400,4 +415,4 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
);
};

export default Activities;
export default Notifications;
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down

0 comments on commit a7aa59d

Please sign in to comment.