mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 02:45:44 +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
@ -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'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'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'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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user