From a7aa59de72bdab41493186d3dae87fe1c1f8afe4 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Wed, 5 Feb 2025 20:13:19 +0100 Subject: [PATCH] Added notification for reposts (#22109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- apps/admin-x-activitypub/package.json | 2 +- apps/admin-x-activitypub/src/MainContent.tsx | 6 +- .../{Activities.tsx => Notifications.tsx} | 83 +++++++++++-------- .../activities/NotificationIcon.tsx | 7 +- .../components/navigation/MainNavigation.tsx | 21 ++++- 5 files changed, 77 insertions(+), 42 deletions(-) rename apps/admin-x-activitypub/src/components/{Activities.tsx => Notifications.tsx} (85%) diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index cce49de186d9..2ed08a852520 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.3.58", + "version": "0.3.59", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-x-activitypub/src/MainContent.tsx b/apps/admin-x-activitypub/src/MainContent.tsx index 43fe5b59d0b0..00abb54af9b3 100644 --- a/apps/admin-x-activitypub/src/MainContent.tsx +++ b/apps/admin-x-activitypub/src/MainContent.tsx @@ -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 ; - case 'activity': - return ; + case 'notifications': + return ; case 'profile': return ; default: diff --git a/apps/admin-x-activitypub/src/components/Activities.tsx b/apps/admin-x-activitypub/src/components/Notifications.tsx similarity index 85% rename from apps/admin-x-activitypub/src/components/Activities.tsx rename to apps/admin-x-activitypub/src/components/Notifications.tsx index b626bb85fc8b..0aa5628d3a9d 100644 --- a/apps/admin-x-activitypub/src/components/Activities.tsx +++ b/apps/admin-x-activitypub/src/components/Notifications.tsx @@ -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 ( -
- ); - } else if (activity.type === ACTIVITY_TYPE.LIKE && !activity.object?.name && activity.object?.content) { - return ( -
- ); - } - - 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 = ({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 {group.object?.name || ''}; + return <>{actorText} liked your {group.object?.type === 'Article' ? 'post' : 'note'} {group.object?.name || ''}; + case ACTIVITY_TYPE.REPOST: + return <>{actorText} reposted your {group.object?.type === 'Article' ? 'post' : 'note'} {group.object?.name || ''}; 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 = ({}) => { +const Notifications: React.FC = () => { const user = 'index'; const [openStates, setOpenStates] = React.useState<{[key: string]: boolean}>({}); @@ -183,7 +173,7 @@ const Activities: React.FC = ({}) => { 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 = ({}) => { 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 = ({}) => { 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 = ({}) => {
- {getGroupDescription(group)} +
- {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) + ) && ( +
+ )} {index < groupedActivities.length - 1 && } @@ -400,4 +415,4 @@ const Activities: React.FC = ({}) => { ); }; -export default Activities; +export default Notifications; diff --git a/apps/admin-x-activitypub/src/components/activities/NotificationIcon.tsx b/apps/admin-x-activitypub/src/components/activities/NotificationIcon.tsx index dc4a73e4bf20..687caa72b3de 100644 --- a/apps/admin-x-activitypub/src/components/activities/NotificationIcon.tsx +++ b/apps/admin-x-activitypub/src/components/activities/NotificationIcon.tsx @@ -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 = ({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 ( diff --git a/apps/admin-x-activitypub/src/components/navigation/MainNavigation.tsx b/apps/admin-x-activitypub/src/components/navigation/MainNavigation.tsx index 21e783a333a9..eaa84d48e091 100644 --- a/apps/admin-x-activitypub/src/components/navigation/MainNavigation.tsx +++ b/apps/admin-x-activitypub/src/components/navigation/MainNavigation.tsx @@ -25,9 +25,24 @@ const MainNavigation: React.FC = ({page}) => { unstyled onClick={() => updateRoute('feed')} /> -
);