sforkowany z mirror/soapbox
Merge remote-tracking branch 'origin/develop' into chats
commit
d12ca77502
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { render, screen, rootState } from '../../jest/test-helpers';
|
||||||
|
import { normalizeStatus, normalizeAccount } from '../../normalizers';
|
||||||
|
import Status from '../status';
|
||||||
|
|
||||||
|
import type { ReducerStatus } from 'soapbox/reducers/statuses';
|
||||||
|
|
||||||
|
const account = normalizeAccount({
|
||||||
|
id: '1',
|
||||||
|
acct: 'alex',
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = normalizeStatus({
|
||||||
|
id: '1',
|
||||||
|
account,
|
||||||
|
content: 'hello world',
|
||||||
|
contentHtml: 'hello world',
|
||||||
|
}) as ReducerStatus;
|
||||||
|
|
||||||
|
describe('<Status />', () => {
|
||||||
|
const state = rootState.setIn(['accounts', '1'], account);
|
||||||
|
|
||||||
|
it('renders content', () => {
|
||||||
|
render(<Status status={status} />, undefined, state);
|
||||||
|
screen.getByText(/hello world/i);
|
||||||
|
expect(screen.getByTestId('status')).toHaveTextContent(/hello world/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the Status Action Bar', () => {
|
||||||
|
it('is rendered', () => {
|
||||||
|
render(<Status status={status} />, undefined, state);
|
||||||
|
expect(screen.getByTestId('status-action-bar')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is not rendered if status is under review', () => {
|
||||||
|
const inReviewStatus = normalizeStatus({ ...status, visibility: 'self' });
|
||||||
|
render(<Status status={inReviewStatus as ReducerStatus} />, undefined, state);
|
||||||
|
expect(screen.queryAllByTestId('status-action-bar')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -22,7 +22,13 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account }) => {
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = (e) => {
|
const handleClick: React.MouseEventHandler = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
history.push(`/timeline/${account.domain}`);
|
|
||||||
|
const timelineUrl = `/timeline/${account.domain}`;
|
||||||
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
|
history.push(timelineUrl);
|
||||||
|
} else {
|
||||||
|
window.open(timelineUrl, '_blank');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -219,7 +225,7 @@ const Account = ({
|
||||||
<Text tag='span' theme='muted' size='sm'>·</Text>
|
<Text tag='span' theme='muted' size='sm'>·</Text>
|
||||||
|
|
||||||
{timestampUrl ? (
|
{timestampUrl ? (
|
||||||
<Link to={timestampUrl} className='hover:underline'>
|
<Link to={timestampUrl} className='hover:underline' onClick={(event) => event.stopPropagation()}>
|
||||||
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
|
<RelativeTimestamp timestamp={timestamp} theme='muted' size='sm' className='whitespace-nowrap' futureDate={futureTimestamp} />
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -143,12 +143,14 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
||||||
|
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
|
|
||||||
if (typeof action === 'function') {
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
|
||||||
action(e);
|
if (to) {
|
||||||
} else if (to) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.history.push(to);
|
this.props.history.push(to);
|
||||||
|
} else if (typeof action === 'function') {
|
||||||
|
e.preventDefault();
|
||||||
|
action(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,6 +194,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
||||||
data-index={i}
|
data-index={i}
|
||||||
target={newTab ? '_blank' : undefined}
|
target={newTab ? '_blank' : undefined}
|
||||||
data-method={isLogout ? 'delete' : undefined}
|
data-method={isLogout ? 'delete' : undefined}
|
||||||
|
title={text}
|
||||||
>
|
>
|
||||||
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none' />}
|
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none' />}
|
||||||
|
|
||||||
|
|
|
@ -49,12 +49,13 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
|
|
||||||
const isEditing = useAppSelector(state => state.compose.get('compose-modal')?.id !== null);
|
const isEditing = useAppSelector(state => state.compose.get('compose-modal')?.id !== null);
|
||||||
|
|
||||||
const handleKeyUp = useCallback((e) => {
|
const visible = !!children;
|
||||||
if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27)
|
|
||||||
&& !!children) {
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) {
|
||||||
handleOnClose();
|
handleOnClose();
|
||||||
}
|
}
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const handleOnClose = () => {
|
const handleOnClose = () => {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
|
@ -147,6 +148,8 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
window.addEventListener('keyup', handleKeyUp, false);
|
window.addEventListener('keyup', handleKeyUp, false);
|
||||||
window.addEventListener('keydown', handleKeyDown, false);
|
window.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
|
||||||
|
@ -154,7 +157,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [visible]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!children && !prevChildren) {
|
if (!!children && !prevChildren) {
|
||||||
|
@ -183,8 +186,6 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const visible = !!children;
|
|
||||||
|
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return (
|
return (
|
||||||
<div className='z-50 transition-all' ref={ref} style={{ opacity: 0 }} />
|
<div className='z-50 transition-all' ref={ref} style={{ opacity: 0 }} />
|
||||||
|
|
|
@ -44,7 +44,12 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
||||||
const account = status.account as AccountEntity;
|
const account = status.account as AccountEntity;
|
||||||
|
|
||||||
if (!compose && e.button === 0) {
|
if (!compose && e.button === 0) {
|
||||||
history.push(`/@${account.acct}/posts/${status.id}`);
|
const statusUrl = `/@${account.acct}/posts/${status.id}`;
|
||||||
|
if (!(e.ctrlKey || e.metaKey)) {
|
||||||
|
history.push(statusUrl);
|
||||||
|
} else {
|
||||||
|
window.open(statusUrl, '_blank');
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,12 +279,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleCopy: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
const { uri } = status;
|
const { uri } = status;
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
textarea.textContent = uri;
|
textarea.textContent = uri;
|
||||||
textarea.style.position = 'fixed';
|
textarea.style.position = 'fixed';
|
||||||
|
|
||||||
document.body.appendChild(textarea);
|
document.body.appendChild(textarea);
|
||||||
|
@ -459,7 +459,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
text: intl.formatMessage(messages.admin_status),
|
text: intl.formatMessage(messages.admin_status),
|
||||||
href: `/pleroma/admin/#/statuses/${status.id}/`,
|
href: `/pleroma/admin/#/statuses/${status.id}/`,
|
||||||
icon: require('@tabler/icons/pencil.svg'),
|
icon: require('@tabler/icons/pencil.svg'),
|
||||||
action: (event) => event.stopPropagation(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -552,6 +551,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
data-testid='status-action-bar'
|
||||||
className={classNames('flex flex-row', {
|
className={classNames('flex flex-row', {
|
||||||
'justify-between': space === 'expand',
|
'justify-between': space === 'expand',
|
||||||
'space-x-2': space === 'compact',
|
'space-x-2': space === 'compact',
|
||||||
|
|
|
@ -50,7 +50,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
||||||
// The typical case with a reply-to and a list of mentions.
|
// The typical case with a reply-to and a list of mentions.
|
||||||
const accounts = to.slice(0, 2).map(account => {
|
const accounts = to.slice(0, 2).map(account => {
|
||||||
const link = (
|
const link = (
|
||||||
<Link to={`/@${account.acct}`} className='reply-mentions__account'>@{account.username}</Link>
|
<Link to={`/@${account.acct}`} className='reply-mentions__account' onClick={(e) => e.stopPropagation()}>@{account.username}</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (hoverable) {
|
if (hoverable) {
|
||||||
|
|
|
@ -84,6 +84,8 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
|
|
||||||
const actualStatus = getActualStatus(status);
|
const actualStatus = getActualStatus(status);
|
||||||
|
|
||||||
|
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.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
didShowCard.current = Boolean(!muted && !hidden && status?.card);
|
didShowCard.current = Boolean(!muted && !hidden && status?.card);
|
||||||
|
@ -97,11 +99,17 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
setShowMedia(!showMedia);
|
setShowMedia(!showMedia);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (): void => {
|
const handleClick = (e?: React.MouseEvent): void => {
|
||||||
if (onClick) {
|
e?.stopPropagation();
|
||||||
onClick();
|
|
||||||
|
if (!e || !(e.ctrlKey || e.metaKey)) {
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
} else {
|
||||||
|
history.push(statusUrl);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`);
|
window.open(statusUrl, '_blank');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -145,7 +153,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHotkeyOpen = (): void => {
|
const handleHotkeyOpen = (): void => {
|
||||||
history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`);
|
history.push(statusUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHotkeyOpenProfile = (): void => {
|
const handleHotkeyOpenProfile = (): void => {
|
||||||
|
@ -292,11 +300,9 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
react: handleHotkeyReact,
|
react: handleHotkeyReact,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`;
|
|
||||||
|
|
||||||
const accountAction = props.accountAction || reblogElement;
|
const accountAction = props.accountAction || reblogElement;
|
||||||
|
|
||||||
const inReview = actualStatus.visibility === 'self';
|
const isUnderReview = actualStatus.visibility === 'self';
|
||||||
const isSensitive = actualStatus.hidden;
|
const isSensitive = actualStatus.hidden;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -307,7 +313,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
data-featured={featured ? 'true' : null}
|
data-featured={featured ? 'true' : null}
|
||||||
aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)}
|
aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)}
|
||||||
ref={node}
|
ref={node}
|
||||||
onClick={() => history.push(statusUrl)}
|
onClick={handleClick}
|
||||||
role='link'
|
role='link'
|
||||||
>
|
>
|
||||||
{featured && (
|
{featured && (
|
||||||
|
@ -354,11 +360,11 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
<Stack
|
<Stack
|
||||||
className={
|
className={
|
||||||
classNames('relative', {
|
classNames('relative', {
|
||||||
'min-h-[220px]': inReview || isSensitive,
|
'min-h-[220px]': isUnderReview || isSensitive,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{(inReview || isSensitive) && (
|
{(isUnderReview || isSensitive) && (
|
||||||
<SensitiveContentOverlay
|
<SensitiveContentOverlay
|
||||||
status={status}
|
status={status}
|
||||||
visible={showMedia}
|
visible={showMedia}
|
||||||
|
@ -392,7 +398,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{!hideActionBar && (
|
{(!hideActionBar && !isUnderReview) && (
|
||||||
<div className='pt-4'>
|
<div className='pt-4'>
|
||||||
<StatusActionBar status={actualStatus} withDismiss={withDismiss} />
|
<StatusActionBar status={actualStatus} withDismiss={withDismiss} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -147,7 +147,7 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deltaX + deltaY < 5 && e.button === 0 && onClick) {
|
if (deltaX + deltaY < 5 && e.button === 0 && !(e.ctrlKey || e.metaKey) && onClick) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
||||||
|
|
||||||
const me = useAppSelector((state) => state.me);
|
const me = useAppSelector((state) => state.me);
|
||||||
|
|
||||||
const renderTranslate = /* translationEnabled && */ me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
const renderTranslate = me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||||
|
|
||||||
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
const handleTranslate: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
@ -134,6 +134,7 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
const status = useAppSelector(state => getStatus(state, { id: props.params.statusId }));
|
||||||
const displayMedia = settings.get('displayMedia') as DisplayMedia;
|
const displayMedia = settings.get('displayMedia') as DisplayMedia;
|
||||||
|
const isUnderReview = status?.visibility === 'self';
|
||||||
|
|
||||||
const { ancestorsIds, descendantsIds } = useAppSelector(state => {
|
const { ancestorsIds, descendantsIds } = useAppSelector(state => {
|
||||||
let ancestorsIds = ImmutableOrderedSet<string>();
|
let ancestorsIds = ImmutableOrderedSet<string>();
|
||||||
|
@ -412,7 +413,7 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
if (next && status) {
|
if (next && status) {
|
||||||
dispatch(fetchNext(status.id, next)).then(({ next }) => {
|
dispatch(fetchNext(status.id, next)).then(({ next }) => {
|
||||||
setNext(next);
|
setNext(next);
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}
|
}
|
||||||
}, 300, { leading: true }), [next, status]);
|
}, 300, { leading: true }), [next, status]);
|
||||||
|
|
||||||
|
@ -475,14 +476,18 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
onOpenCompareHistoryModal={handleOpenCompareHistoryModal}
|
onOpenCompareHistoryModal={handleOpenCompareHistoryModal}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr className='mb-2 border-t-2 dark:border-primary-800' />
|
{!isUnderReview ? (
|
||||||
|
<>
|
||||||
|
<hr className='mb-2 border-t-2 dark:border-primary-800' />
|
||||||
|
|
||||||
<StatusActionBar
|
<StatusActionBar
|
||||||
status={status}
|
status={status}
|
||||||
expandable={false}
|
expandable={false}
|
||||||
space='expand'
|
space='expand'
|
||||||
withLabels
|
withLabels
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClo
|
||||||
className={classNames('w-full', { active, destructive })}
|
className={classNames('w-full', { active, destructive })}
|
||||||
data-method={isLogout ? 'delete' : null}
|
data-method={isLogout ? 'delete' : null}
|
||||||
>
|
>
|
||||||
{icon && <Icon className='min-w-fit' title={text} src={icon} role='presentation' tabIndex={-1} />}
|
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
||||||
<div>
|
<div>
|
||||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||||
<div>{meta}</div>
|
<div>{meta}</div>
|
||||||
|
|
|
@ -311,7 +311,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.svg-icon:first-child {
|
.svg-icon:first-child {
|
||||||
@apply w-5 h-5 mr-2.5;
|
@apply min-w-[1.25rem] w-5 h-5 mr-2.5;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
|
|
Ładowanie…
Reference in New Issue