Move status info to above Account

This commit is contained in:
Chewbacca 2023-01-05 13:44:05 -05:00
parent 1412385382
commit 024b161504
4 changed files with 132 additions and 87 deletions

View file

@ -2,7 +2,7 @@ import classNames from 'clsx';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
import { NavLink, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { mentionCompose, replyCompose } from 'soapbox/actions/compose'; import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions'; import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
@ -16,12 +16,14 @@ import { useAppDispatch, useSettings } from 'soapbox/hooks';
import { defaultMediaVisibility, textForScreenReader, getActualStatus } from 'soapbox/utils/status'; import { defaultMediaVisibility, textForScreenReader, getActualStatus } from 'soapbox/utils/status';
import EventPreview from './event-preview'; import EventPreview from './event-preview';
import RelativeTimestamp from './relative-timestamp';
import StatusActionBar from './status-action-bar'; import StatusActionBar from './status-action-bar';
import StatusContent from './status-content'; import StatusContent from './status-content';
import StatusMedia from './status-media'; import StatusMedia from './status-media';
import StatusReplyMentions from './status-reply-mentions'; import StatusReplyMentions from './status-reply-mentions';
import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
import { Card, HStack, Stack, Text } from './ui'; import StatusInfo from './statuses/status-info';
import { Card, Stack, Text } from './ui';
import type { import type {
Account as AccountEntity, Account as AccountEntity,
@ -37,6 +39,7 @@ const messages = defineMessages({
export interface IStatus { export interface IStatus {
id?: string, id?: string,
avatarSize?: number,
status: StatusEntity, status: StatusEntity,
onClick?: () => void, onClick?: () => void,
muted?: boolean, muted?: boolean,
@ -56,6 +59,7 @@ export interface IStatus {
const Status: React.FC<IStatus> = (props) => { const Status: React.FC<IStatus> = (props) => {
const { const {
status, status,
avatarSize = 42,
focusable = true, focusable = true,
hoverable = true, hoverable = true,
onClick, onClick,
@ -84,7 +88,7 @@ const Status: React.FC<IStatus> = (props) => {
const [minHeight, setMinHeight] = useState(208); const [minHeight, setMinHeight] = useState(208);
const actualStatus = getActualStatus(status); const actualStatus = getActualStatus(status);
const isReblog = status.reblog && typeof status.reblog === 'object';
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
// Track height changes we know about to compensate scrolling. // Track height changes we know about to compensate scrolling.
@ -201,8 +205,49 @@ const Status: React.FC<IStatus> = (props) => {
firstEmoji?.focus(); firstEmoji?.focus();
}; };
const renderStatusInfo = () => {
if (isReblog) {
return (
<StatusInfo
avatarSize={avatarSize}
to={`/@${status.getIn(['account', 'acct'])}`}
icon={<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />}
text={
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} reposted'
values={{
name: (
<bdi className='truncate pr-1 rtl:pl-1'>
<strong
className='text-gray-800 dark:text-gray-200'
dangerouslySetInnerHTML={{
__html: String(status.getIn(['account', 'display_name_html'])),
}}
/>
</bdi>
),
}}
/>
}
/>
);
} else if (featured) {
return (
<StatusInfo
avatarSize={avatarSize}
icon={<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />}
text={
<Text size='xs' theme='muted' weight='medium'>
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
</Text>
}
/>
);
}
};
if (!status) return null; if (!status) return null;
let rebloggedByText, reblogElement, reblogElementMobile;
if (hidden) { if (hidden) {
return ( return (
@ -228,55 +273,8 @@ const Status: React.FC<IStatus> = (props) => {
); );
} }
let rebloggedByText;
if (status.reblog && typeof status.reblog === 'object') { if (status.reblog && typeof status.reblog === 'object') {
const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) };
reblogElement = (
<NavLink
to={`/@${status.getIn(['account', 'acct'])}`}
onClick={(event) => event.stopPropagation()}
className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 rtl:space-x-reverse hover:underline'
>
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
<HStack alignItems='center'>
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} reposted'
values={{
name: <bdi className='max-w-[100px] truncate pr-1 rtl:px-1'>
<strong className='text-gray-800 dark:text-gray-200' dangerouslySetInnerHTML={displayNameHtml} />
</bdi>,
}}
/>
</HStack>
</NavLink>
);
reblogElementMobile = (
<div className='pb-5 -mt-2 sm:hidden truncate'>
<NavLink
to={`/@${status.getIn(['account', 'acct'])}`}
onClick={(event) => event.stopPropagation()}
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline'
>
<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />
<span>
<FormattedMessage
id='status.reblogged_by'
defaultMessage='{name} reposted'
values={{
name: <bdi>
<strong className='text-gray-800 dark:text-gray-200' dangerouslySetInnerHTML={displayNameHtml} />
</bdi>,
}}
/>
</span>
</NavLink>
</div>
);
rebloggedByText = intl.formatMessage( rebloggedByText = intl.formatMessage(
messages.reblogged_by, messages.reblogged_by,
{ name: String(status.getIn(['account', 'acct'])) }, { name: String(status.getIn(['account', 'acct'])) },
@ -312,7 +310,13 @@ const Status: React.FC<IStatus> = (props) => {
react: handleHotkeyReact, react: handleHotkeyReact,
}; };
const accountAction = props.accountAction || reblogElement; const timestampEl = (
<Link to={statusUrl} className='hover:underline' onClick={(event) => event.stopPropagation()}>
<RelativeTimestamp timestamp={actualStatus.created_at} theme='muted' size='sm' className='whitespace-nowrap' />
</Link>
);
const accountAction = props.accountAction || timestampEl;
const isUnderReview = actualStatus.visibility === 'self'; const isUnderReview = actualStatus.visibility === 'self';
const isSensitive = actualStatus.hidden; const isSensitive = actualStatus.hidden;
@ -328,21 +332,9 @@ const Status: React.FC<IStatus> = (props) => {
onClick={handleClick} onClick={handleClick}
role='link' role='link'
> >
{featured && (
<div className='pt-4 px-4'>
<HStack alignItems='center' space={1}>
<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />
<Text size='sm' theme='muted' weight='medium'>
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
</Text>
</HStack>
</div>
)}
<Card <Card
variant={variant} variant={variant}
className={classNames('status__wrapper', `status-${actualStatus.visibility}`, { className={classNames('status__wrapper space-y-4', `status-${actualStatus.visibility}`, {
'py-6 sm:p-5': variant === 'rounded', 'py-6 sm:p-5': variant === 'rounded',
'status-reply': !!status.in_reply_to_id, 'status-reply': !!status.in_reply_to_id,
muted, muted,
@ -350,21 +342,18 @@ const Status: React.FC<IStatus> = (props) => {
})} })}
data-id={status.id} data-id={status.id}
> >
{reblogElementMobile} {renderStatusInfo()}
<div className='mb-4'>
<AccountContainer <AccountContainer
key={String(actualStatus.getIn(['account', 'id']))} key={String(actualStatus.getIn(['account', 'id']))}
id={String(actualStatus.getIn(['account', 'id']))} id={String(actualStatus.getIn(['account', 'id']))}
timestamp={actualStatus.created_at}
timestampUrl={statusUrl}
action={accountAction} action={accountAction}
hideActions={!accountAction} hideActions={!accountAction}
showEdit={!!actualStatus.edited_at} showEdit={!!actualStatus.edited_at}
showProfileHoverCard={hoverable} showProfileHoverCard={hoverable}
withLinkToProfile={hoverable} withLinkToProfile={hoverable}
avatarSize={avatarSize}
/> />
</div>
<div className='status__content-wrapper'> <div className='status__content-wrapper'>
<StatusReplyMentions status={actualStatus} hoverable={hoverable} /> <StatusReplyMentions status={actualStatus} hoverable={hoverable} />

View file

@ -0,0 +1,38 @@
import React from 'react';
import { Link } from 'react-router-dom';
interface IStatusInfo {
avatarSize: number
to?: string
icon: React.ReactNode
text: React.ReactNode
}
const StatusInfo = (props: IStatusInfo) => {
const { avatarSize, to, icon, text } = props;
const onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
event.stopPropagation();
};
const Container = to ? Link : 'div';
const containerProps: any = to ? { onClick, to } : {};
return (
<Container
{...containerProps}
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-3 rtl:space-x-reverse hover:underline'
>
<div
className='flex justify-end'
style={{ width: avatarSize }}
>
{icon}
</div>
{text}
</Container>
);
};
export default StatusInfo;

View file

@ -151,6 +151,8 @@ const buildMessage = (
}); });
}; };
const avatarSize = 48;
interface INotificaton { interface INotificaton {
hidden?: boolean, hidden?: boolean,
notification: NotificationEntity, notification: NotificationEntity,
@ -290,7 +292,7 @@ const Notification: React.FC<INotificaton> = (props) => {
<AccountContainer <AccountContainer
id={account.id} id={account.id}
hidden={hidden} hidden={hidden}
avatarSize={48} avatarSize={avatarSize}
/> />
) : null; ) : null;
case 'follow_request': case 'follow_request':
@ -298,7 +300,7 @@ const Notification: React.FC<INotificaton> = (props) => {
<AccountContainer <AccountContainer
id={account.id} id={account.id}
hidden={hidden} hidden={hidden}
avatarSize={48} avatarSize={avatarSize}
actionType='follow_request' actionType='follow_request'
/> />
) : null; ) : null;
@ -307,7 +309,7 @@ const Notification: React.FC<INotificaton> = (props) => {
<AccountContainer <AccountContainer
id={notification.target.id} id={notification.target.id}
hidden={hidden} hidden={hidden}
avatarSize={48} avatarSize={avatarSize}
/> />
) : null; ) : null;
case 'favourite': case 'favourite':
@ -327,6 +329,7 @@ const Notification: React.FC<INotificaton> = (props) => {
hidden={hidden} hidden={hidden}
onMoveDown={handleMoveDown} onMoveDown={handleMoveDown}
onMoveUp={handleMoveUp} onMoveUp={handleMoveUp}
avatarSize={avatarSize}
/> />
) : null; ) : null;
default: default:
@ -358,13 +361,18 @@ const Notification: React.FC<INotificaton> = (props) => {
> >
<div className='p-4 focusable'> <div className='p-4 focusable'>
<div className='mb-2'> <div className='mb-2'>
<HStack alignItems='center' space={1.5}> <HStack alignItems='center' space={3}>
<div
className='flex justify-end'
style={{ flexBasis: avatarSize }}
>
{renderIcon()} {renderIcon()}
</div>
<div className='truncate'> <div className='truncate'>
<Text <Text
theme='muted' theme='muted'
size='sm' size='xs'
truncate truncate
data-testid='message' data-testid='message'
> >

View file

@ -3,6 +3,7 @@ import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import Account from 'soapbox/components/account'; import Account from 'soapbox/components/account';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
import StatusContent from 'soapbox/components/status-content'; import StatusContent from 'soapbox/components/status-content';
import StatusMedia from 'soapbox/components/status-media'; import StatusMedia from 'soapbox/components/status-media';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
@ -58,6 +59,15 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
const isUnderReview = actualStatus.visibility === 'self'; const isUnderReview = actualStatus.visibility === 'self';
const isSensitive = actualStatus.hidden; const isSensitive = actualStatus.hidden;
const timestampEl = (
<RelativeTimestamp
timestamp={actualStatus.created_at}
theme='muted'
size='sm'
className='whitespace-nowrap'
/>
);
let statusTypeIcon = null; let statusTypeIcon = null;
let quote; let quote;
@ -87,7 +97,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
<Account <Account
key={account.id} key={account.id}
account={account} account={account}
timestamp={actualStatus.created_at} action={timestampEl}
avatarSize={42} avatarSize={42}
hideActions hideActions
/> />