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

@ -3,6 +3,7 @@ import ActivityItem from './activities/ActivityItem';
import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png'; import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
import FeedItem from './feed/FeedItem'; import FeedItem from './feed/FeedItem';
import MainNavigation from './navigation/MainNavigation'; import MainNavigation from './navigation/MainNavigation';
import NewPostModal from './modals/NewPostModal';
import NiceModal from '@ebay/nice-modal-react'; import NiceModal from '@ebay/nice-modal-react';
import React, {useEffect, useRef} from 'react'; import React, {useEffect, useRef} from 'react';
import Separator from './global/Separator'; import Separator from './global/Separator';
@ -10,9 +11,10 @@ import ViewProfileModal from './global/ViewProfileModal';
import getName from '../utils/get-name'; import getName from '../utils/get-name';
import getUsername from '../utils/get-username'; import getUsername from '../utils/get-username';
import useSuggestedProfiles from '../hooks/useSuggestedProfiles'; 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 {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system';
import {handleViewContent} from '../utils/content-handlers'; 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'; import {useRouting} from '@tryghost/admin-x-framework/routing';
type Layout = 'inbox' | 'feed'; type Layout = 'inbox' | 'feed';
@ -71,10 +73,12 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
}; };
}, [hasNextPage, isFetchingNextPage, fetchNextPage]); }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const {data: user} = useUserDataForUser('index');
return ( return (
<> <>
<MainNavigation page={layout}/> <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'> <div className='w-full px-8'>
{isLoading ? ( {isLoading ? (
<div className='flex flex-col items-center justify-center space-y-4 text-center'> <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 ? ( ) : activities.length > 0 ? (
<> <>
<div className={`mx-auto flex items-start gap-8`}> <div className={`mx-auto flex items-start gap-8`}>
<div className='flex w-full min-w-0 items-start'> <div className='flex w-full min-w-0 flex-col items-center'>
<ul className={`mx-auto flex w-full flex-col ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}> <div className={`flex w-full min-w-0 flex-col items-start ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}>
{activities.map((activity, index) => ( {layout === 'feed' && <div className='relative mx-[-12px] mb-4 mt-10 flex w-[calc(100%+24px)] items-center p-3'>
<li <div className=''>
key={activity.id} <APAvatar author={user as ActorProperties} />
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> </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)} />
</ul> </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>
<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> <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> <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> <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' : ''}`} className={`relative z-[9998] ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
hideLabel={true} hideLabel={true}
icon='dotdotdot' icon='dotdotdot'
iconColorClass={`(${layout === 'inbox' ? 'text-grey-900' : 'text-grey-600'}`} iconColorClass={`${layout === 'inbox' ? 'text-grey-900' : 'text-grey-500'}`}
id='more' id='more'
size='sm' size='sm'
unstyled={true} unstyled={true}
@ -237,7 +237,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<span className='z-10'>{actor.name} reposted</span> <span className='z-10'>{actor.name} reposted</span>
</div>} </div>}
<div className={`border-1 flex flex-col gap-2.5`} data-test-activity> <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}/> <APAvatar author={author}/>
<div className='flex min-w-0 flex-col gap-0.5'> <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> <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}) => { const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
let iconSize = 18; let iconSize = 18;
let containerClass = 'shrink-0 items-center justify-center relative z-10 flex'; 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`; 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 = ''; let badgeColor = '';
const [iconUrl, setIconUrl] = useState(author?.icon?.url); const [iconUrl, setIconUrl] = useState(author?.icon?.url);
@ -39,23 +39,23 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
switch (size) { switch (size) {
case '2xs': case '2xs':
iconSize = 10; iconSize = 10;
containerClass = clsx('h-4 w-4 rounded ', containerClass); containerClass = clsx('h-4 w-4 rounded-md ', containerClass);
imageClass = 'z-10 rounded w-4 h-4 object-cover'; imageClass = 'z-10 rounded-md w-4 h-4 object-cover';
break; break;
case 'xs': case 'xs':
iconSize = 12; iconSize = 12;
containerClass = clsx('h-5 w-5 rounded ', containerClass); containerClass = clsx('h-5 w-5 rounded-md ', containerClass);
imageClass = 'z-10 rounded w-5 h-5 object-cover'; imageClass = 'z-10 rounded-md w-5 h-5 object-cover';
break; break;
case 'sm': case 'sm':
containerClass = clsx('h-10 w-10 rounded', containerClass); containerClass = clsx('h-10 w-10 rounded-md', containerClass);
break; break;
case 'lg': case 'lg':
containerClass = clsx('h-22 w-22 rounded-xl', containerClass); containerClass = clsx('h-22 w-22 rounded-xl', containerClass);
imageClass = 'z-10 rounded-xl w-22 h-22 object-cover'; imageClass = 'z-10 rounded-xl w-22 h-22 object-cover';
break; break;
default: default:
containerClass = clsx('h-10 w-10 rounded', containerClass); containerClass = clsx('h-10 w-10 rounded-md', containerClass);
break; 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;