Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a8790cc
Changed select option typing
PedroMarianoAlmeida Jun 12, 2026
666844d
Cleaned select prop type formatting
PedroMarianoAlmeida Jun 12, 2026
900e55a
Merge branch 'main' into changed-select-option-typing
PedroMarianoAlmeida Jun 12, 2026
6e75631
Merge branch 'main' into changed-select-option-typing
9larsons Jun 12, 2026
4fd2009
Changed ActivityPub API types
PedroMarianoAlmeida Jun 12, 2026
9fc1715
Fixed confirm unload test typing
PedroMarianoAlmeida Jun 12, 2026
b76361b
Merge branch 'main' into remove-any-from-activypub
PedroMarianoAlmeida Jun 12, 2026
1c09895
coderabit suggestion
PedroMarianoAlmeida Jun 12, 2026
a5744da
remove unknow type too
PedroMarianoAlmeida Jun 12, 2026
7093f8e
Merge branch 'remove-any-from-activypub' of https://github.com/PedroM…
PedroMarianoAlmeida Jun 12, 2026
99bd01e
Restored assertion failure messages in use-confirm-unload test
9larsons Jun 13, 2026
7fe37c0
Merge branch 'main' into remove-any-from-activypub
9larsons Jun 13, 2026
969e6f4
implement remainder types
PedroMarianoAlmeida Jun 13, 2026
228e298
Merge branch 'main' into remove-any-from-activypub
PedroMarianoAlmeida Jun 13, 2026
bb40e3b
Merge branch 'main' into remove-any-from-activypub
PedroMarianoAlmeida Jun 14, 2026
6c3b460
Fixed the other CodeRabbit suggestion
PedroMarianoAlmeida Jun 14, 2026
e223389
Merge branch 'main' into remove-any-from-activypub
PedroMarianoAlmeida Jun 14, 2026
61ce981
Added mapped ActivityPub prop coverage
PedroMarianoAlmeida Jun 14, 2026
34c7e53
Merge branch 'remove-any-from-activypub' of https://github.com/PedroM…
PedroMarianoAlmeida Jun 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/activitypub/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/activitypub",
"version": "3.1.37",
"version": "3.1.40",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
15 changes: 11 additions & 4 deletions apps/activitypub/src/components/feed/feed-item.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions apps/activitypub/src/utils/pending-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 13 additions & 8 deletions apps/activitypub/src/views/notifications/notifications.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -113,6 +113,7 @@ function groupNotifications(notifications: Notification[]): NotificationGroup[]
const NotificationGroupDescription: React.FC<NotificationGroupDescriptionProps> = ({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';

Expand All @@ -125,13 +126,15 @@ const NotificationGroupDescription: React.FC<NotificationGroupDescriptionProps>
className={actorClass}
onClick={(e) => {
e?.stopPropagation();
handleProfileClick(firstActor.handle, navigate);
if (firstActor.handle) {
handleProfileClick(firstActor.handle, navigate);
}
}}
>
{firstActor.name}
</span>
</ProfilePreviewHoverCard>
{hasOthers && ` and ${otherActors.length} ${otherActors.length > 1 ? 'others' : 'other'}`}
{hasOthers && ` and ${formatNumber(otherActorsCount)} ${otherActorsCount > 1 ? 'others' : 'other'}`}
</>
);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -349,7 +352,7 @@ const Notifications: React.FC = () => {
))}
{group.actors.length > maxAvatars && (!openStates[group.id || `${group.type}_${index}`]) && (
<div className='absolute right-[28px] z-10 flex size-9 items-center justify-center rounded-full bg-black/50 text-base font-semibold tracking-tightest text-white'>
{`+${group.actors.length - maxAvatars}`}
{`+${formatNumber(group.actors.length - maxAvatars)}`}
</div>
)}

Expand All @@ -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);
}
}}
>
<div className='flex min-w-0 items-center'>
Expand All @@ -386,7 +391,7 @@ const Notifications: React.FC = () => {
<span className='ml-2 line-clamp-1 text-base font-semibold group-hover/item:underline dark:text-white'>{actor.name}</span>
<span className='ml-1 line-clamp-1 text-base text-gray-700 dark:text-gray-600'>{actor.handle}</span>
</div>
{group.type === 'follow' && !actor.followedByMe && (
{group.type === 'follow' && !actor.followedByMe && actor.handle && (
<FollowButton
following={false}
handle={actor.handle}
Expand Down Expand Up @@ -418,7 +423,7 @@ const Notifications: React.FC = () => {
}
</div>
{/* 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 && (
<FollowButton
following={false}
handle={group.actors[0].handle}
Expand Down
39 changes: 39 additions & 0 deletions apps/activitypub/test/unit/utils/pending-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ describe('Pending Activity Utils', function () {
expect(pendingActivity.object.preview!.type).toBe('Note');
expect(pendingActivity.object.preview!.content).toBe(content);
expect(pendingActivity.object.id).toBe(id);
expect(pendingActivity.object.attachment).toEqual([]);
expect(pendingActivity.object.replyCount).toBe(0);
expect(pendingActivity.object.liked).toBe(false);
expect(pendingActivity.object.reposted).toBe(false);
expect(pendingActivity.object.repostCount).toBe(0);
expect(pendingActivity.object.authored).toBe(true);
expect(pendingActivity.object.attributedTo).toBeDefined();
expect(pendingActivity.object.attributedTo).toBeInstanceOf(Object);

Expand All @@ -80,6 +86,39 @@ describe('Pending Activity Utils', function () {
expect(attributedToName).toEqual(actor.name);
expect(attributedToPreferredUsername).toEqual(actor.preferredUsername);
});

it('should attach an uploaded image url to the pending activity', function () {
const actor: ActorProperties = {
id: 'https://example.com/actor',
icon: {
url: 'https://example.com/icon.png'
},
name: 'Example Actor',
preferredUsername: 'example',
'@context': '',
discoverable: false,
featured: '',
followers: '',
following: '',
image: {url: ''},
inbox: '',
manuallyApprovesFollowers: false,
outbox: '',
publicKey: {
id: '',
owner: '',
publicKeyPem: ''
},
published: '',
summary: '',
type: 'Person',
url: 'https://example.com/actor'
};
const id = generatePendingActivityId();
const pendingActivity = generatePendingActivity(actor, id, 'Pending note', 'https://example.com/image.png');

expect(pendingActivity.object.image).toBe('https://example.com/image.png');
});
});

describe('formatPendingActivityContent', function () {
Expand Down
41 changes: 41 additions & 0 deletions apps/activitypub/test/unit/utils/posts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,49 @@ describe('mapPostToActivity', function () {
expect(object.preview.content).toBe('Test Excerpt');
expect(object.id).toBe('123');
expect(object.replyCount).toBe(3);
expect(object.likeCount).toBe(2);
expect(object.liked).toBe(true);
expect(object.reposted).toBe(false);
expect(object.repostCount).toBe(5);
expect(object.authored).toBe(true);
});

test('it preserves object metadata', function () {
const object = mapPostToActivity({
...post,
metadata: {
ghostAuthors: [{
name: 'Ghost Author',
profile_image: 'https://example.com/authors/ghost-author.jpg'
}]
}
}).object;

expect(object.metadata).toEqual({
ghostAuthors: [{
name: 'Ghost Author',
profile_image: 'https://example.com/authors/ghost-author.jpg'
}]
});
});

test('it maps object engagement properties', function () {
const object = mapPostToActivity({
...post,
replyCount: 0,
likeCount: 0,
likedByMe: false,
repostedByMe: true,
repostCount: 0,
authoredByMe: false
}).object;

expect(object.replyCount).toBe(0);
expect(object.likeCount).toBe(0);
expect(object.liked).toBe(false);
expect(object.reposted).toBe(true);
expect(object.repostCount).toBe(0);
expect(object.authored).toBe(false);
});

test('it sets the correct attachments', function () {
Expand Down Expand Up @@ -180,6 +220,7 @@ describe('mapPostToActivity', function () {
});

expect(activity.actor.followedByMe).toBe(true);
expect(activity.actor.handle).toBe('@testuser@example.com');

// Test for reposts
const repostActivity = mapPostToActivity({
Expand Down
48 changes: 38 additions & 10 deletions apps/admin-x-framework/src/api/activitypub.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
import {createMutation, createQueryWithId} from '../utils/api/hooks';
import {JSONObject} from './config';

export type ActivityPubContext = string | (string | JSONObject)[];

export type ActivityPubAttachment = {
type: string;
mediaType?: string;
name?: string;
url: string;
};

export type ObjectMetadata = {
ghostAuthors?: Array<{
name: string;
profile_image: string;
}>;
};

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;
Expand All @@ -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;
Expand All @@ -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 = {
Expand Down
Loading