mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-21 15:35:00 +03:00
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:
parent
49c0e60053
commit
3abff38a53
@ -20,4 +20,4 @@ const App: React.FC<AppProps> = ({framework, designSystem}) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
@ -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'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'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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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's new?'
|
||||
rows={1}
|
||||
>
|
||||
</textarea>
|
||||
</FormPrimitive.Control>
|
||||
</FormPrimitive.Field>
|
||||
</div>
|
||||
</FormPrimitive.Root>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default NewPostModal;
|
@ -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')} />
|
||||
|
Loading…
Reference in New Issue
Block a user