0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Connected the endpoint for publishing notes (#21680)

close
https://linear.app/ghost/issue/AP-601/allow-users-to-publish-short-form-content-as-notes

---------

Co-authored-by: Michael Barrett <mike@ghost.org>
This commit is contained in:
Djordje Vlaisavljevic 2024-11-21 20:00:01 +00:00 committed by GitHub
parent 0ac36bd324
commit 0861c524df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 127 additions and 18 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@tryghost/admin-x-activitypub",
"version": "0.3.21",
"version": "0.3.22",
"license": "MIT",
"repository": {
"type": "git",

View file

@ -1336,4 +1336,33 @@ describe('ActivityPubAPI', function () {
expect(actual).toEqual(expected);
});
});
describe('note', function () {
test('It creates a note and returns it', async function () {
const fakeFetch = Fetch({
[`https://activitypub.api/.ghost/activitypub/actions/note`]: {
async assert(_resource, init) {
expect(init?.method).toEqual('POST');
expect(init?.body).toEqual('{"content":"Hello, world!"}');
},
response: JSONResponse({
id: 'https://example.com/note/abc123'
})
}
});
const api = new ActivityPubAPI(
new URL('https://activitypub.api'),
new URL('https://auth.api'),
'index',
fakeFetch
);
const result = await api.note('Hello, world!');
expect(result).toEqual({
id: 'https://example.com/note/abc123'
});
});
});
});

View file

@ -325,6 +325,12 @@ export class ActivityPubAPI {
return response;
}
async note(content: string) {
const url = new URL('.ghost/activitypub/actions/note', this.apiUrl);
const response = await this.fetchJSON(url, 'POST', {content});
return response;
}
get userApiUrl() {
return new URL(`.ghost/activitypub/users/${this.handle}`, this.apiUrl);
}

View file

@ -95,7 +95,7 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
</div>
<Button aria-label='New post' className='text absolute inset-0 w-full rounded-lg bg-white pl-[64px] text-left text-[1.5rem] tracking-normal text-grey-500 shadow-[0_0_1px_rgba(0,0,0,.32),0_1px_6px_rgba(0,0,0,.03),0_8px_10px_-8px_rgba(0,0,0,.16)] transition-all hover:shadow-[0_0_1px_rgba(0,0,0,.32),0_1px_6px_rgba(0,0,0,.03),0_8px_10px_-8px_rgba(0,0,0,.26)]' label='What&apos;s new?' unstyled onClick={() => NiceModal.show(NewPostModal)} />
</div>}
<ul className={`mx-auto flex w-full flex-col`}>
<ul className={`mx-auto flex w-full flex-col ${layout === 'inbox' && 'mt-3'}`}>
{activities.map((activity, index) => (
<li
key={activity.id}

View file

@ -3,42 +3,76 @@ import APAvatar from '../global/APAvatar';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Modal, showToast} from '@tryghost/admin-x-design-system';
import {useUserDataForUser} from '../../hooks/useActivityPubQueries';
import {useNoteMutationForUser, useUserDataForUser} from '../../hooks/useActivityPubQueries';
import {useState} from 'react';
const NewPostModal = NiceModal.create(() => {
const modal = useModal();
const {data: user} = useUserDataForUser('index');
const noteMutation = useNoteMutationForUser('index');
const [content, setContent] = useState('');
const isDisabled = noteMutation.isLoading || !content.trim();
const handlePost = async () => {
const trimmedContent = content.trim();
if (!trimmedContent) {
return;
}
await noteMutation.mutate({content: trimmedContent}, {
onSuccess() {
showToast({
message: 'Note posted',
type: 'success'
});
modal.remove();
},
onError() {
showToast({
message: 'An error occurred while posting your note.',
type: 'error'
});
}
});
};
const handleCancel = () => {
modal.remove();
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
};
return (
<Modal
cancelLabel="Cancel"
okColor="black"
okDisabled={isDisabled}
okLabel="Post"
size="md"
stickyFooter={true}
width={575}
onCancel={() => {
modal.remove();
}}
onOk={() => {
showToast({
message: 'Note sent',
type: 'success'
});
modal.remove();
}}
onCancel={handleCancel}
onOk={handlePost}
>
<div className='flex items-start gap-2'>
<APAvatar author={user as ActorProperties} />
<FormPrimitive.Root asChild>
<div className='flex w-full flex-col'>
<FormPrimitive.Field name='temp' asChild>
<FormPrimitive.Field name='content' asChild>
<FormPrimitive.Control asChild>
<textarea
autoFocus={true}
className='ap-textarea w-full resize-none p-2 text-[1.5rem]'
className='ap-textarea w-full resize-none bg-transparent p-2 text-[1.5rem]'
disabled={noteMutation.isLoading}
placeholder='What&apos;s new?'
rows={1}
>
</textarea>
rows={3}
value={content}
onChange={handleChange}
/>
</FormPrimitive.Control>
</FormPrimitive.Field>
</div>

View file

@ -436,3 +436,43 @@ export function useThreadForUser(handle: string, id: string) {
return {threadQuery, addToThread};
}
export function useNoteMutationForUser(handle: string) {
const queryClient = useQueryClient();
return useMutation({
async mutationFn({content}: {content: string}) {
const siteUrl = await getSiteUrl();
const api = createActivityPubAPI(handle, siteUrl);
return await api.note(content) as Activity;
},
onSuccess: (activity: Activity) => {
queryClient.setQueryData([`outbox:${handle}`], (current?: Activity[]) => {
if (current === undefined) {
return current;
}
return [activity, ...current];
});
queryClient.setQueriesData([`activities:${handle}`], (current?: {pages: {data: Activity[]}[]}) => {
if (current === undefined) {
return current;
}
return {
...current,
pages: current.pages.map((page: {data: Activity[]}, index: number) => {
if (index === 0) {
return {
...page,
data: [activity, ...page.data]
};
}
return page;
})
};
});
}
});
}