diff --git a/apps/activitypub/package.json b/apps/activitypub/package.json index df58781804d..3a5b6b9349a 100644 --- a/apps/activitypub/package.json +++ b/apps/activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/activitypub", - "version": "3.1.37", + "version": "3.1.40", "license": "MIT", "repository": { "type": "git", diff --git a/apps/activitypub/src/components/feed/feed-item.tsx b/apps/activitypub/src/components/feed/feed-item.tsx index b6482bb7859..eb3bee1438d 100644 --- a/apps/activitypub/src/components/feed/feed-item.tsx +++ b/apps/activitypub/src/components/feed/feed-item.tsx @@ -1,6 +1,6 @@ import FeedItemMenu from './feed-item-menu'; import React, {useEffect, useRef, useState} from 'react'; -import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; +import {ActivityPubAttachment, ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {Button, Skeleton} from '@tryghost/shade/components'; import {H4} from '@tryghost/shade/primitives'; import {LucideIcon} from '@tryghost/shade/utils'; @@ -20,11 +20,18 @@ import {renderTimestamp} from '../../utils/render-timestamp'; import {useDeleteMutationForUser, useFollowMutationForUser, useUnfollowMutationForUser} from '../../hooks/use-activity-pub-queries'; import {useNavigateWithBasePath} from '@src/hooks/use-navigate-with-base-path'; -export function getAttachment(object: ObjectProperties) { - let attachment; +export function getAttachment(object: ObjectProperties): ActivityPubAttachment | ActivityPubAttachment[] | null { + let attachment: ActivityPubAttachment | ActivityPubAttachment[] | undefined; if (object.image) { - attachment = object.image; + attachment = typeof object.image === 'string' ? { + type: 'Image', + url: object.image + } : { + type: object.image.type ?? 'Image', + mediaType: object.image.mediaType, + url: object.image.url + }; } if (object.type === 'Note' && !attachment) { diff --git a/apps/activitypub/src/utils/pending-activity.ts b/apps/activitypub/src/utils/pending-activity.ts index d1d104ccc3b..9164e28bca7 100644 --- a/apps/activitypub/src/utils/pending-activity.ts +++ b/apps/activitypub/src/utils/pending-activity.ts @@ -58,6 +58,7 @@ export function generatePendingActivity(actorProps: ActorProperties, id: string, // These are used in the app, but are not part of the ObjectProperties type id, replyCount: 0, + likeCount: 0, liked: false, reposted: false, repostCount: 0, diff --git a/apps/activitypub/src/views/notifications/notifications.tsx b/apps/activitypub/src/views/notifications/notifications.tsx index acdebf11587..3a36ab5d251 100644 --- a/apps/activitypub/src/views/notifications/notifications.tsx +++ b/apps/activitypub/src/views/notifications/notifications.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useRef} from 'react'; import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {Button, LoadingIndicator, Skeleton} from '@tryghost/shade/components'; -import {LucideIcon} from '@tryghost/shade/utils'; +import {LucideIcon, formatNumber} from '@tryghost/shade/utils'; import APAvatar from '@components/global/ap-avatar'; import AppError from '@components/layout/error'; @@ -113,6 +113,7 @@ function groupNotifications(notifications: Notification[]): NotificationGroup[] const NotificationGroupDescription: React.FC = ({group}) => { const [firstActor, ...otherActors] = group.actors; const hasOthers = otherActors.length > 0; + const otherActorsCount = otherActors.length; const actorClass = 'cursor-pointer font-semibold hover:underline text-black dark:text-white'; @@ -125,13 +126,15 @@ const NotificationGroupDescription: React.FC className={actorClass} onClick={(e) => { e?.stopPropagation(); - handleProfileClick(firstActor.handle, navigate); + if (firstActor.handle) { + handleProfileClick(firstActor.handle, navigate); + } }} > {firstActor.name} - {hasOthers && ` and ${otherActors.length} ${otherActors.length > 1 ? 'others' : 'other'}`} + {hasOthers && ` and ${formatNumber(otherActorsCount)} ${otherActorsCount > 1 ? 'others' : 'other'}`} ); @@ -271,7 +274,7 @@ const Notifications: React.FC = () => { case 'follow': if (group.actors.length > 1) { toggleOpen(group.id || `${group.type}_${index}`); - } else { + } else if (group.actors[0]?.handle) { handleProfileClick(group.actors[0].handle, navigate); } break; @@ -349,7 +352,7 @@ const Notifications: React.FC = () => { ))} {group.actors.length > maxAvatars && (!openStates[group.id || `${group.type}_${index}`]) && (
- {`+${group.actors.length - maxAvatars}`} + {`+${formatNumber(group.actors.length - maxAvatars)}`}
)} @@ -372,7 +375,9 @@ const Notifications: React.FC = () => { className='group/item break-anywhere flex items-center justify-between gap-4' onClick={(e) => { e?.stopPropagation(); - handleProfileClick(actor.handle, navigate); + if (actor.handle) { + handleProfileClick(actor.handle, navigate); + } }} >
@@ -386,7 +391,7 @@ const Notifications: React.FC = () => { {actor.name} {actor.handle}
- {group.type === 'follow' && !actor.followedByMe && ( + {group.type === 'follow' && !actor.followedByMe && actor.handle && ( { } {/* Follow button for singular follow, reply, and mention */} - {group.actors.length === 1 && (group.type === 'follow' || group.type === 'reply' || group.type === 'mention') && !group.actors[0].followedByMe && ( + {group.actors.length === 1 && (group.type === 'follow' || group.type === 'reply' || group.type === 'mention') && !group.actors[0].followedByMe && group.actors[0].handle && ( ; +}; export type FollowItem = { id: string; - preferredUsername: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any + preferredUsername: string; }; export type ObjectProperties = { - '@context': string | (string | object)[]; + '@context': ActivityPubContext; + id: string; type: 'Article' | 'Link' | 'Note' | 'Tombstone'; name: string; content: string | null; summary: string | null; url?: string | undefined; - attributedTo?: object | string | object[] | undefined; + attributedTo?: ActorProperties | string | ActorProperties[] | JSONObject | JSONObject[]; image?: string | { url: string; mediaType?: string; type?: string; }; + attachment?: ActivityPubAttachment | ActivityPubAttachment[]; published?: string; + createdAt?: string; preview?: {type: string, content: string | null}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; + replyCount: number; + likeCount: number; + liked?: boolean; + reposted?: boolean; + repostCount: number; + authored: boolean; + metadata?: ObjectMetadata; } export type ActorProperties = { - '@context': string | (string | object)[]; + '@context': ActivityPubContext; attachment?: { type: string; name: string; @@ -49,6 +72,13 @@ export type ActorProperties = { name: string; outbox: string; preferredUsername: string; + handle?: string; + followedByMe?: boolean; + followsMe?: boolean; + followingCount?: number; + followerCount?: number; + bio?: string; + avatarUrl?: string | null; publicKey: { id: string; owner: string; @@ -58,8 +88,6 @@ export type ActorProperties = { summary: string; type: 'Person'; url: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; } export type Activity = {