Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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.35",
"version": "3.1.36",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
10 changes: 5 additions & 5 deletions apps/activitypub/src/components/feed/feed-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import ProfilePreviewHoverCard from '../global/profile-preview-hover-card';

import FeedItemStats from './feed-item-stats';
import clsx from 'clsx';
import getHandle from '../../utils/get-handle';
import getReadingTime from '../../utils/get-reading-time';
import getUsername from '../../utils/get-username';
import {handleProfileClick} from '../../utils/handle-profile-click';
import {openLinksInNewTab, sanitizeHtml, stripHtml} from '../../utils/content-formatters';
import {renderTimestamp} from '../../utils/render-timestamp';
Expand Down Expand Up @@ -379,7 +379,7 @@ const FeedItem: React.FC<FeedItemProps> = ({
author = typeof object.attributedTo === 'object' ? object.attributedTo as ActorProperties : actor;
}

const authorHandle = author ? getUsername(author) : null;
const authorHandle = author ? getHandle(author) : null;
Comment thread
sagzy marked this conversation as resolved.

const followedByMe = author?.followedByMe || false;

Expand Down Expand Up @@ -455,7 +455,7 @@ const FeedItem: React.FC<FeedItemProps> = ({
</span>
<div className={`flex w-full text-md text-gray-700 dark:text-gray-600`}>
<span className={`truncate ${!isPending ? 'hover-underline' : ''}`}>
{!isLoading ? getUsername(author) : <Skeleton className='w-56' />}
{!isLoading ? getHandle(author) : <Skeleton className='w-56' />}
</span>
<div className={`ml-1 before:mr-1 ${!isLoading && 'before:content-["·"]'}`} title={`${timestamp}`}>
{!isLoading ? renderTimestamp(object, (isPending === false && !object.authored)) : <Skeleton className='w-4' />}
Expand Down Expand Up @@ -564,7 +564,7 @@ const FeedItem: React.FC<FeedItemProps> = ({
<div>{renderTimestamp(object, !object.authored)}</div>
</div>
<div className='flex w-full'>
<span className='min-w-0 truncate text-gray-700 dark:text-gray-600'>{getUsername(author)}</span>
<span className='min-w-0 truncate text-gray-700 dark:text-gray-600'>{getHandle(author)}</span>
</div>
</div>
</>}
Expand Down Expand Up @@ -626,7 +626,7 @@ const FeedItem: React.FC<FeedItemProps> = ({
<div>{renderTimestamp(object, (isPending === false && !object.authored))}</div>
</div>
<div className='flex'>
<span className='truncate text-gray-700'>{getUsername(author)}</span>
<span className='truncate text-gray-700'>{getHandle(author)}</span>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/activitypub/src/components/global/ap-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useEffect, useState} from 'react';
import clsx from 'clsx';
import getUsername from '@utils/get-username';
import getHandle from '@utils/get-handle';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Skeleton} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
Expand Down Expand Up @@ -140,7 +140,7 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, isLoading = false, dis
return <Skeleton className={imageClass} containerClassName={containerClass} />;
}

const handle = author?.handle || getUsername(author as ActorProperties);
const handle = getHandle(author as ActorProperties);

const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
Expand Down
4 changes: 2 additions & 2 deletions apps/activitypub/src/components/global/ap-reply-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {HTMLProps, useState} from 'react';

import APAvatar from './ap-avatar';
import NewNoteModal from '@components/modals/new-note-modal';
import getUsername from '../../utils/get-username';
import getHandle from '../../utils/get-handle';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {useUserDataForUser} from '@hooks/use-activity-pub-queries';

Expand All @@ -29,7 +29,7 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
const attributedTo = object.attributedTo as ActorProperties | undefined;
let placeholder = 'Reply...';
if (attributedTo?.preferredUsername && attributedTo?.id) {
placeholder = `Reply to ${getUsername(attributedTo)}...`;
placeholder = `Reply to ${getHandle(attributedTo)}...`;
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import FollowButton from './follow-button';
import React, {useEffect, useState} from 'react';
import getUsername from '../../utils/get-username';
import getHandle from '../../utils/get-handle';
import {Account} from '@src/api/activitypub';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Avatar, AvatarFallback, AvatarImage, Badge, HoverCard, HoverCardContent, HoverCardTrigger, Skeleton} from '@tryghost/shade/components';
Expand Down Expand Up @@ -36,7 +36,7 @@ const ProfilePreviewHoverCard: React.FC<ProfilePreviewHoverCardProps> = ({

let targetHandle = actor?.handle;
if (!targetHandle && actor && isActorProperties(actor)) {
targetHandle = getUsername(actor);
targetHandle = getHandle(actor);
}

const bypassHover = disabled || (!targetHandle && !actor);
Expand Down
4 changes: 2 additions & 2 deletions apps/activitypub/src/components/modals/new-note-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as FormPrimitive from '@radix-ui/react-form';
import APAvatar from '@components/global/ap-avatar';
import FeedItem from '@components/feed/feed-item';
import getUsername from '@utils/get-username';
import getHandle from '@utils/get-handle';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, Input, LoadingIndicator, Skeleton} from '@tryghost/shade/components';
import {ChangeEvent, useCallback, useEffect, useRef, useState} from 'react';
Expand Down Expand Up @@ -273,7 +273,7 @@ const NewNoteModal: React.FC<NewNoteModalProps> = ({children, replyTo, onReply,
if (replyTo) {
const attributedTo = replyTo.object.attributedTo || {};
if (typeof attributedTo === 'object' && 'preferredUsername' in attributedTo && 'id' in attributedTo) {
placeholder = `Reply to ${getUsername(attributedTo as ActorProperties)}...`;
placeholder = `Reply to ${getHandle(attributedTo as ActorProperties)}...`;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
function getUsername(actor: {preferredUsername: string; id: string|null;}) {
function getHandle(actor: {handle?: string; preferredUsername: string; id: string|null;}) {
if (actor.handle) {
return actor.handle;
}

if (!actor.preferredUsername || !actor.id) {
return '@unknown@unknown';
}
Expand All @@ -9,4 +13,4 @@ function getUsername(actor: {preferredUsername: string; id: string|null;}) {
}
}

export default getUsername;
export default getHandle;
4 changes: 2 additions & 2 deletions apps/activitypub/src/utils/handle-profile-click.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import getUsername from './get-username';
import getHandle from './get-handle';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {useNavigate} from '@tryghost/admin-x-framework';

Expand All @@ -7,6 +7,6 @@ export const handleProfileClick = (actor: ActorProperties | string, navigate: Re
if (typeof actor === 'string') {
navigate(`/profile/${actor}`);
} else {
navigate(`/profile/${actor.handle || getUsername(actor)}`);
navigate(`/profile/${getHandle(actor)}`);
}
};
2 changes: 2 additions & 0 deletions apps/activitypub/src/utils/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export function mapPostToActivity(post: Post): Activity {
url: post.author.avatarUrl
},
name: post.author.name,
handle: post.author.handle,
preferredUsername: post.author.handle.split('@')[1],
Comment thread
sagzy marked this conversation as resolved.
followedByMe: post.author.followedByMe,
// These are not used but needed to comply with the ActorProperties type
Expand Down Expand Up @@ -55,6 +56,7 @@ export function mapPostToActivity(post: Post): Activity {
url: post.repostedBy.avatarUrl
},
name: post.repostedBy.name,
handle: post.repostedBy.handle,
preferredUsername: post.repostedBy.handle.split('@')[1],
followedByMe: post.repostedBy.followedByMe,
// These are not used but needed to comply with the ActorProperties type
Expand Down
4 changes: 2 additions & 2 deletions apps/activitypub/src/views/feed/note.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Layout from '@src/components/layout/layout';
import ProfilePreviewHoverCard from '@components/global/profile-preview-hover-card';
import React, {useEffect, useRef, useState} from 'react';
import ShowRepliesButton from '@src/components/global/show-replies-button';
import getUsername from '@src/utils/get-username';
import getHandle from '@src/utils/get-handle';
import {Activity} from '@tryghost/admin-x-framework/api/activitypub';
import {EmptyViewIcon, EmptyViewIndicator} from '@src/components/global/empty-view-indicator';
import {LoadingIndicator, Skeleton} from '@tryghost/shade/components';
Expand Down Expand Up @@ -206,7 +206,7 @@ const Note = () => {
<span className='min-w-0 truncate font-semibold whitespace-nowrap hover:underline'>{currentPost.actor.name}</span>
</div>
<div className='flex w-full'>
<span className='truncate text-gray-700 after:mx-1 after:font-normal after:text-gray-700 after:content-["·"]'>{getUsername(currentPost.actor)}</span>
<span className='truncate text-gray-700 after:mx-1 after:font-normal after:text-gray-700 after:content-["·"]'>{getHandle(currentPost.actor)}</span>
<span className='text-gray-700'>{renderTimestamp(object, !object.authored)}</span>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/activitypub/src/views/inbox/components/reader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Customizer, {COLOR_OPTIONS, type ColorOption, type FontSize, useCustomizerSettings} from './customizer';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import ShowRepliesButton from '@src/components/global/show-replies-button';
import getUsername from '../../../utils/get-username';
import getHandle from '../../../utils/get-handle';
import {LoadingIndicator, Skeleton} from '@tryghost/shade/components';

import {renderTimestamp} from '../../../utils/render-timestamp';
Expand Down Expand Up @@ -824,7 +824,7 @@ export const Reader: React.FC<ReaderProps> = ({
<span className='min-w-0 truncate font-semibold whitespace-nowrap text-black hover:underline dark:text-white'>{isLoadingContent ? <Skeleton className='w-20' /> : actor.name}</span>
</div>
<div className='flex w-full'>
{!isLoadingContent && <span className='truncate text-gray-700 after:mx-1 after:font-normal after:text-gray-700 after:content-["·"]'>{getUsername(actor)}</span>}
{!isLoadingContent && <span className='truncate text-gray-700 after:mx-1 after:font-normal after:text-gray-700 after:content-["·"]'>{getHandle(actor)}</span>}
<span className='text-gray-700'>{isLoadingContent ? <Skeleton className='w-[120px]' /> : renderTimestamp(object, !object.authored)}</span>
</div>
</div>
Expand All @@ -833,7 +833,7 @@ export const Reader: React.FC<ReaderProps> = ({
{!object.authored && !isLoadingContent && (
<FollowButton
following={actor.followedByMe ?? false}
handle={getUsername(actor)}
handle={getHandle(actor)}
/>
)}
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/activitypub/src/views/profile/components/actor-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import ActivityItem from '@src/components/activities/activity-item';
import FollowButton from '@src/components/global/follow-button';
import ProfilePreviewHoverCard from '@components/global/profile-preview-hover-card';
import React, {useEffect, useRef} from 'react';
import getHandle from '@src/utils/get-handle';
import getName from '@src/utils/get-name';
import getUsername from '@src/utils/get-username';
import {Actor} from '@src/api/activitypub';
import {Button, LoadingIndicator, NoValueLabel, NoValueLabelIcon} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
Expand Down Expand Up @@ -70,7 +70,7 @@ const ActorList: React.FC<ActorListProps> = ({
) : (
<div className='flex flex-col'>
{actors.map(({actor, isFollowing, blockedByMe, domainBlockedByMe}) => {
const actorHandle = actor.handle || getUsername(actor);
const actorHandle = getHandle(actor);
const isCurrentUser = actorHandle === currentUser?.handle;

return (
Expand Down
48 changes: 48 additions & 0 deletions apps/activitypub/test/unit/utils/get-handle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import getHandle from '../../../src/utils/get-handle';

describe('getHandle', function () {
it('returns the API-provided handle when present', async function () {
const user = {
handle: '@index@activitypub.example',
preferredUsername: 'index',
id: 'https://www.platformer.news/'
};

const result = getHandle(user);

expect(result).toBe('@index@activitypub.example');
});

it('returns the formatted handle', async function () {
const user = {
preferredUsername: 'index',
id: 'https://www.platformer.news/'
};

const result = getHandle(user);

expect(result).toBe('@index@platformer.news');
});

it('returns a default handle if the user object is missing data', async function () {
const user = {
preferredUsername: '',
id: ''
};

const result = getHandle(user);

expect(result).toBe('@unknown@unknown');
});

it('returns a default handle if url parsing fails', async function () {
const user = {
preferredUsername: 'index',
id: 'not-a-url'
};

const result = getHandle(user);

expect(result).toBe('@unknown@unknown');
});
});
36 changes: 0 additions & 36 deletions apps/activitypub/test/unit/utils/get-username.test.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions apps/activitypub/test/unit/utils/posts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('mapPostToActivity', function () {
expect(actor.id).toBe('https://example.com/users/123');
expect(actor.icon.url).toBe('https://example.com/users/123/avatar.jpg');
expect(actor.name).toBe('Test User');
expect(actor.handle).toBe('@testuser@example.com');
expect(actor.preferredUsername).toBe('testuser');

// When the post has been reposted, the actor should be the reposter
Expand All @@ -92,9 +93,25 @@ describe('mapPostToActivity', function () {
expect(actor.id).toBe('https://example.com/users/456');
expect(actor.icon.url).toBe('https://example.com/users/456/avatar.jpg');
expect(actor.name).toBe('Test User 2');
expect(actor.handle).toBe('@testuser2@example.com');
expect(actor.preferredUsername).toBe('testuser2');
});

test('it preserves the API-provided author handle', function () {
const actor = mapPostToActivity({
...post,
author: {
...post.author,
handle: '@testuser@social.example',
url: 'https://example.com/users/123'
}
}).actor;

expect(actor.id).toBe('https://example.com/users/123');
expect(actor.handle).toBe('@testuser@social.example');
expect(actor.preferredUsername).toBe('testuser');
});

test('it sets the correct object type', function () {
expect(
mapPostToActivity(post).object.type
Expand Down Expand Up @@ -124,6 +141,7 @@ describe('mapPostToActivity', function () {
expect(object.summary).toBe('Test Summary');
expect(object.url).toBe('https://example.com/posts/123');
expect(object.attributedTo.id).toBe('https://example.com/users/123');
expect(object.attributedTo.handle).toBe('@testuser@example.com');
expect(object.published).toBe('2024-01-01T00:00:00Z');
expect(object.preview.content).toBe('Test Excerpt');
expect(object.id).toBe('123');
Expand Down
Loading