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:
parent
0ac36bd324
commit
0861c524df
6 changed files with 127 additions and 18 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@tryghost/admin-x-activitypub",
|
||||
"version": "0.3.21",
|
||||
"version": "0.3.22",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'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}
|
||||
|
|
|
@ -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's new?'
|
||||
rows={1}
|
||||
>
|
||||
</textarea>
|
||||
rows={3}
|
||||
value={content}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormPrimitive.Control>
|
||||
</FormPrimitive.Field>
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue