Added UI for publishing short-form notes from Ghost admin (#21667)

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

- Added a button to the top of the feed that opens a modal that lets you write and short post

---------

Co-authored-by: Michael Barrett <mike@ghost.org>
This commit is contained in:
Djordje Vlaisavljevic 2024-11-21 12:07:55 +00:00 committed by GitHub
parent 49c0e60053
commit 3abff38a53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 115 additions and 51 deletions

View File

@ -20,4 +20,4 @@ const App: React.FC<AppProps> = ({framework, designSystem}) => {
);
};
export default App;
export default App;

View File

@ -3,6 +3,7 @@ import ActivityItem from './activities/ActivityItem';
import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
import FeedItem from './feed/FeedItem';
import MainNavigation from './navigation/MainNavigation';
import NewPostModal from './modals/NewPostModal';
import NiceModal from '@ebay/nice-modal-react';
import React, {useEffect, useRef} from 'react';
import Separator from './global/Separator';
@ -10,9 +11,10 @@ import ViewProfileModal from './global/ViewProfileModal';
import getName from '../utils/get-name';
import getUsername from '../utils/get-username';
import useSuggestedProfiles from '../hooks/useSuggestedProfiles';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system';
import {handleViewContent} from '../utils/content-handlers';
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
import {useActivitiesForUser, useUserDataForUser} from '../hooks/useActivityPubQueries';
import {useRouting} from '@tryghost/admin-x-framework/routing';
type Layout = 'inbox' | 'feed';
@ -71,10 +73,12 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
};
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const {data: user} = useUserDataForUser('index');
return (
<>
<MainNavigation page={layout}/>
<div className='z-0 my-5 flex w-full flex-col'>
<div className='z-0 mb-5 flex w-full flex-col'>
<div className='w-full px-8'>
{isLoading ? (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
@ -83,36 +87,45 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
) : activities.length > 0 ? (
<>
<div className={`mx-auto flex items-start gap-8`}>
<div className='flex w-full min-w-0 items-start'>
<ul className={`mx-auto flex w-full flex-col ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}>
{activities.map((activity, index) => (
<li
key={activity.id}
data-test-view-article
>
<FeedItem
actor={activity.actor}
commentCount={activity.object.replyCount ?? 0}
layout={layout}
object={activity.object}
type={activity.type}
onClick={() => handleViewContent(activity, false, updateActivity)}
onCommentClick={() => handleViewContent(activity, true, updateActivity)}
/>
{index < activities.length - 1 && (
<Separator />
)}
</li>
))}
<div ref={loadMoreRef} className='h-1'></div>
{isFetchingNextPage && (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
<div className='flex w-full min-w-0 flex-col items-center'>
<div className={`flex w-full min-w-0 flex-col items-start ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}>
{layout === 'feed' && <div className='relative mx-[-12px] mb-4 mt-10 flex w-[calc(100%+24px)] items-center p-3'>
<div className=''>
<APAvatar author={user as ActorProperties} />
</div>
)}
</ul>
<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`}>
{activities.map((activity, index) => (
<li
key={activity.id}
data-test-view-article
>
<FeedItem
actor={activity.actor}
commentCount={activity.object.replyCount ?? 0}
layout={layout}
object={activity.object}
type={activity.type}
onClick={() => handleViewContent(activity, false, updateActivity)}
onCommentClick={() => handleViewContent(activity, true, updateActivity)}
/>
{index < activities.length - 1 && (
<Separator />
)}
</li>
))}
<div ref={loadMoreRef} className='h-1'></div>
{isFetchingNextPage && (
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
<LoadingIndicator size='md' />
</div>
)}
</ul>
</div>
</div>
<div className='sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden xxxl:sticky xxxl:right-[40px]'>
<div className='sticky top-[133px] ml-auto w-full max-w-[300px] max-lg:hidden xxxl:sticky xxxl:right-[40px]'>
{/* <Icon className='mb-2' colorClass='text-blue-500' name='comment' size='md' /> */}
<h2 className='mb-2 text-lg font-semibold'>This is your {layout === 'inbox' ? 'inbox' : 'feed'}</h2>
<p className='mb-6 border-b border-grey-200 pb-6 text-grey-700'>You&apos;ll find {layout === 'inbox' ? 'long-form content' : 'short posts and updates'} from the accounts you follow here.</p>
<h2 className='mb-2 text-lg font-semibold'>You might also like</h2>

View File

@ -220,7 +220,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
className={`relative z-[9998] ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
hideLabel={true}
icon='dotdotdot'
iconColorClass={`(${layout === 'inbox' ? 'text-grey-900' : 'text-grey-600'}`}
iconColorClass={`${layout === 'inbox' ? 'text-grey-900' : 'text-grey-500'}`}
id='more'
size='sm'
unstyled={true}
@ -237,7 +237,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<span className='z-10'>{actor.name} reposted</span>
</div>}
<div className={`border-1 flex flex-col gap-2.5`} data-test-activity>
<div className='flex min-w-0 items-center gap-2.5'>
<div className='flex min-w-0 items-center gap-3'>
<APAvatar author={author}/>
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='min-w-0 truncate break-all font-semibold leading-[normal]' data-test-activity-heading>{author.name}</span>

View File

@ -15,7 +15,7 @@ interface APAvatarProps {
const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
let iconSize = 18;
let containerClass = 'shrink-0 items-center justify-center relative z-10 flex';
let imageClass = 'z-10 rounded w-10 h-10 object-cover';
let imageClass = 'z-10 rounded-md w-10 h-10 object-cover';
const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-[0.6rem] border-2 border-white content-box flex items-center justify-center`;
let badgeColor = '';
const [iconUrl, setIconUrl] = useState(author?.icon?.url);
@ -39,23 +39,23 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
switch (size) {
case '2xs':
iconSize = 10;
containerClass = clsx('h-4 w-4 rounded ', containerClass);
imageClass = 'z-10 rounded w-4 h-4 object-cover';
containerClass = clsx('h-4 w-4 rounded-md ', containerClass);
imageClass = 'z-10 rounded-md w-4 h-4 object-cover';
break;
case 'xs':
iconSize = 12;
containerClass = clsx('h-5 w-5 rounded ', containerClass);
imageClass = 'z-10 rounded w-5 h-5 object-cover';
containerClass = clsx('h-5 w-5 rounded-md ', containerClass);
imageClass = 'z-10 rounded-md w-5 h-5 object-cover';
break;
case 'sm':
containerClass = clsx('h-10 w-10 rounded', containerClass);
containerClass = clsx('h-10 w-10 rounded-md', containerClass);
break;
case 'lg':
containerClass = clsx('h-22 w-22 rounded-xl', containerClass);
imageClass = 'z-10 rounded-xl w-22 h-22 object-cover';
break;
default:
containerClass = clsx('h-10 w-10 rounded', containerClass);
containerClass = clsx('h-10 w-10 rounded-md', containerClass);
break;
}

View File

@ -0,0 +1,51 @@
import * as FormPrimitive from '@radix-ui/react-form';
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';
const NewPostModal = NiceModal.create(() => {
const modal = useModal();
const {data: user} = useUserDataForUser('index');
return (
<Modal
cancelLabel="Cancel"
okLabel="Post"
stickyFooter={true}
width={575}
onCancel={() => {
modal.remove();
}}
onOk={() => {
showToast({
message: 'Note sent',
type: 'success'
});
modal.remove();
}}
>
<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.Control asChild>
<textarea
autoFocus={true}
className='ap-textarea w-full resize-none p-2 text-[1.5rem]'
placeholder='What&apos;s new?'
rows={1}
>
</textarea>
</FormPrimitive.Control>
</FormPrimitive.Field>
</div>
</FormPrimitive.Root>
</div>
</Modal>
);
});
export default NewPostModal;

View File

@ -16,24 +16,24 @@ const MainNavigation: React.FC<MainNavigationProps> = ({page}) => {
queryClient.removeQueries({
queryKey: ['activities:index']
});
updateRoute(newRoute);
};
return (
<MainHeader>
<div className='col-[1/2] flex gap-8 px-8'>
<Button
className={`${page === 'inbox' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Inbox'
unstyled
onClick={() => handleRouteChange('inbox')}
<Button
className={`${page === 'inbox' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Inbox'
unstyled
onClick={() => handleRouteChange('inbox')}
/>
<Button
className={`${page === 'feed' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Feed'
unstyled
onClick={() => handleRouteChange('feed')}
<Button
className={`${page === 'feed' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`}
label='Feed'
unstyled
onClick={() => handleRouteChange('feed')}
/>
<Button className={`${page === 'activities' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Notifications' unstyled onClick={() => updateRoute('activity')} />
<Button className={`${page === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Search' unstyled onClick={() => updateRoute('search')} />