Replace glitch-soc's collapsed toots with upstream's “Read more” (#2916)
* Remove glitch-soc's post collapse feature * Get rid of the infamous `parseClick` * Remove unused CSS * Use upstream's “Read More” implementation * Update translation strings
This commit is contained in:
1 parent
28751ff042
commit
d65f6c2f8a
17 files changed
+203
-644
No files matched your search
@@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import ExpandLessIcon from '@/material-icons/400-24px/expand_less.svg?react';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
|
||||
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
|
||||
});
|
||||
|
||||
export const CollapseButton = ({ collapsed, setCollapsed }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleCollapsedClick = useCallback((e) => {
|
||||
if (e.button === 0) {
|
||||
setCollapsed(!collapsed);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, [collapsed, setCollapsed]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
className='status__collapse-button'
|
||||
animate
|
||||
active={collapsed}
|
||||
title={
|
||||
collapsed ?
|
||||
intl.formatMessage(messages.uncollapse) :
|
||||
intl.formatMessage(messages.collapse)
|
||||
}
|
||||
icon='angle-double-up'
|
||||
iconComponent={ExpandLessIcon}
|
||||
onClick={handleCollapsedClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
CollapseButton.propTypes = {
|
||||
collapsed: PropTypes.bool,
|
||||
setCollapsed: PropTypes.func.isRequired,
|
||||
};
|
||||
@@ -24,11 +24,12 @@ import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_conte
|
||||
import { displayMedia } from '../initial_state';
|
||||
|
||||
import AttachmentList from './attachment_list';
|
||||
import { CollapseButton } from './collapse_button';
|
||||
import { Avatar } from './avatar';
|
||||
import { AvatarOverlay } from './avatar_overlay';
|
||||
import { DisplayName } from './display_name';
|
||||
import { getHashtagBarForStatus } from './hashtag_bar';
|
||||
import StatusActionBar from './status_action_bar';
|
||||
import StatusContent from './status_content';
|
||||
import StatusHeader from './status_header';
|
||||
import StatusIcons from './status_icons';
|
||||
import StatusPrepend from './status_prepend';
|
||||
|
||||
@@ -99,6 +100,7 @@ class Status extends ImmutablePureComponent {
|
||||
onEmbed: PropTypes.func,
|
||||
onHeightChange: PropTypes.func,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onToggleCollapsed: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
onInteractionModal: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
@@ -127,8 +129,6 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
state = {
|
||||
isCollapsed: false,
|
||||
autoCollapsed: false,
|
||||
isExpanded: undefined,
|
||||
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
|
||||
revealBehindCW: undefined,
|
||||
@@ -156,19 +156,10 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
updateOnStates = [
|
||||
'isExpanded',
|
||||
'isCollapsed',
|
||||
'showMedia',
|
||||
'forceFilter',
|
||||
];
|
||||
|
||||
// If our settings have changed to disable collapsed statuses, then we
|
||||
// need to make sure that we uncollapse every one. We do that by watching
|
||||
// for changes to `settings.collapsed.enabled` in
|
||||
// `getderivedStateFromProps()`.
|
||||
|
||||
// We also need to watch for changes on the `collapse` prop---if this
|
||||
// changes to anything other than `undefined`, then we need to collapse or
|
||||
// uncollapse our status accordingly.
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
let update = {};
|
||||
let updated = false;
|
||||
@@ -183,30 +174,12 @@ class Status extends ImmutablePureComponent {
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// Update state based on new props
|
||||
if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
|
||||
if (prevState.isCollapsed) {
|
||||
update.isCollapsed = false;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uncollapsing toots when the shared CW state is expanded
|
||||
if (nextProps.settings.getIn(['content_warnings', 'shared_state']) &&
|
||||
nextProps.status?.get('spoiler_text')?.length && nextProps.status?.get('hidden') === false &&
|
||||
prevState.statusPropHidden !== false && prevState.isCollapsed
|
||||
) {
|
||||
update.isCollapsed = false;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
// The “expanded” prop is used to one-off change the local state.
|
||||
// It's used in the thread view when unfolding/re-folding all CWs at once.
|
||||
if (nextProps.expanded !== prevState.expandedProp &&
|
||||
nextProps.expanded !== undefined
|
||||
) {
|
||||
update.isExpanded = nextProps.expanded;
|
||||
if (nextProps.expanded) update.isCollapsed = false;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
@@ -226,63 +199,13 @@ class Status extends ImmutablePureComponent {
|
||||
return updated ? update : null;
|
||||
}
|
||||
|
||||
// When mounting, we just check to see if our status should be collapsed,
|
||||
// and collapse it if so. We don't need to worry about whether collapsing
|
||||
// is enabled here, because `setCollapsed()` already takes that into
|
||||
// account.
|
||||
|
||||
// The cases where a status should be collapsed are:
|
||||
//
|
||||
// - The `collapse` prop has been set to `true`
|
||||
// - The user has decided in local settings to collapse all statuses.
|
||||
// - The user has decided to collapse all notifications ('muted'
|
||||
// statuses).
|
||||
// - The user has decided to collapse long statuses and the status is
|
||||
// over the user set value (default 400 without media, or 610px with).
|
||||
// - The status is a reply and the user has decided to collapse all
|
||||
// replies.
|
||||
// - The status contains media and the user has decided to collapse all
|
||||
// statuses with media.
|
||||
// - The status is a reblog the user has decided to collapse all
|
||||
// statuses which are reblogs.
|
||||
componentDidMount () {
|
||||
const { node } = this;
|
||||
const {
|
||||
status,
|
||||
settings,
|
||||
collapse,
|
||||
muted,
|
||||
prepend,
|
||||
} = this.props;
|
||||
|
||||
// Prevent a crash when node is undefined. Not completely sure why this
|
||||
// happens, might be because status === null.
|
||||
if (node === undefined) return;
|
||||
|
||||
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
|
||||
|
||||
// Don't autocollapse if CW state is shared and status is explicitly revealed,
|
||||
// as it could cause surprising changes when receiving notifications
|
||||
if (settings.getIn(['content_warnings', 'shared_state']) && status.get('spoiler_text').length && !status.get('hidden')) return;
|
||||
|
||||
let autoCollapseHeight = parseInt(autoCollapseSettings.get('height'));
|
||||
if (status.get('media_attachments').size && !muted) {
|
||||
autoCollapseHeight += 210;
|
||||
}
|
||||
|
||||
if (collapse ||
|
||||
autoCollapseSettings.get('all') ||
|
||||
(autoCollapseSettings.get('notifications') && muted) ||
|
||||
(autoCollapseSettings.get('lengthy') && node.clientHeight > autoCollapseHeight) ||
|
||||
(autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by') ||
|
||||
(autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null) ||
|
||||
(autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size > 0)
|
||||
) {
|
||||
this.setCollapsed(true);
|
||||
// Hack to fix timeline jumps on second rendering when auto-collapsing
|
||||
this.setState({ autoCollapsed: true });
|
||||
}
|
||||
|
||||
// Hack to fix timeline jumps when a preview card is fetched
|
||||
this.setState({
|
||||
showCard: !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card') && this.props.settings.get('inline_preview_cards'),
|
||||
@@ -297,16 +220,15 @@ class Status extends ImmutablePureComponent {
|
||||
const { muted, hidden, status, settings } = this.props;
|
||||
|
||||
const doShowCard = !muted && !hidden && status && status.get('card') && settings.get('inline_preview_cards');
|
||||
if (this.state.autoCollapsed || (doShowCard && !this.state.showCard)) {
|
||||
if (doShowCard && !this.state.showCard) {
|
||||
if (doShowCard) this.setState({ showCard: true });
|
||||
if (this.state.autoCollapsed) this.setState({ autoCollapsed: false });
|
||||
return this.props.getScrollPosition();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
componentDidUpdate(prevProps, _prevState, snapshot) {
|
||||
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
|
||||
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||
}
|
||||
@@ -335,72 +257,43 @@ class Status extends ImmutablePureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
// `setCollapsed()` sets the value of `isCollapsed` in our state, that is,
|
||||
// whether the toot is collapsed or not.
|
||||
|
||||
// `setCollapsed()` automatically checks for us whether toot collapsing
|
||||
// is enabled, so we don't have to.
|
||||
setCollapsed = (value) => {
|
||||
if (this.props.settings.getIn(['collapsed', 'enabled'])) {
|
||||
if (value) {
|
||||
this.setExpansion(false);
|
||||
}
|
||||
this.setState({ isCollapsed: value });
|
||||
} else {
|
||||
this.setState({ isCollapsed: false });
|
||||
}
|
||||
};
|
||||
|
||||
setExpansion = (value) => {
|
||||
if (this.props.settings.getIn(['content_warnings', 'shared_state']) && this.props.status.get('hidden') === value) {
|
||||
this.props.onToggleHidden(this.props.status);
|
||||
}
|
||||
|
||||
this.setState({ isExpanded: value });
|
||||
if (value) {
|
||||
this.setCollapsed(false);
|
||||
}
|
||||
};
|
||||
|
||||
// `parseClick()` takes a click event and responds appropriately.
|
||||
// If our status is collapsed, then clicking on it should uncollapse it.
|
||||
// If `Shift` is held, then clicking on it should collapse it.
|
||||
// Otherwise, we open the url handed to us in `destination`, if
|
||||
// applicable.
|
||||
parseClick = (e, destination) => {
|
||||
const { status, history } = this.props;
|
||||
const { isCollapsed } = this.state;
|
||||
if (!history) return;
|
||||
|
||||
if (e.button !== 0 || e.ctrlKey || e.altKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCollapsed) this.setCollapsed(false);
|
||||
else if (e.shiftKey) {
|
||||
this.setCollapsed(true);
|
||||
document.getSelection().removeAllRanges();
|
||||
} else if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
return;
|
||||
} else {
|
||||
if (destination === undefined) {
|
||||
destination = `/@${
|
||||
status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
|
||||
}/${
|
||||
status.getIn(['reblog', 'id'], status.get('id'))
|
||||
}`;
|
||||
}
|
||||
history.push(destination);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.handleHotkeyOpen();
|
||||
};
|
||||
|
||||
handleAccountClick = (e, proper = true) => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
this._openProfile(proper);
|
||||
};
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
|
||||
this.props.onToggleHidden(this.props.status);
|
||||
@@ -466,12 +359,34 @@ class Status extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
handleHotkeyOpen = () => {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
return;
|
||||
}
|
||||
|
||||
const { history } = this.props;
|
||||
const status = this.props.status;
|
||||
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
};
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
this._openProfile();
|
||||
};
|
||||
|
||||
_openProfile = () => {
|
||||
const { history } = this.props;
|
||||
const status = this.props.status;
|
||||
|
||||
if (!history) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}`);
|
||||
};
|
||||
|
||||
handleHotkeyMoveUp = e => {
|
||||
@@ -482,13 +397,6 @@ class Status extends ImmutablePureComponent {
|
||||
this.props.onMoveDown(this.props.containerId || this.props.id, e.target.getAttribute('data-featured'));
|
||||
};
|
||||
|
||||
handleHotkeyCollapse = () => {
|
||||
if (!this.props.settings.getIn(['collapsed', 'enabled']))
|
||||
return;
|
||||
|
||||
this.setCollapsed(!this.state.isCollapsed);
|
||||
};
|
||||
|
||||
handleHotkeyToggleSensitive = () => {
|
||||
this.handleToggleMediaVisibility();
|
||||
};
|
||||
@@ -506,6 +414,10 @@ class Status extends ImmutablePureComponent {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
handleCollapsedToggle = isCollapsed => {
|
||||
this.props.onToggleCollapsed(this.props.status, isCollapsed);
|
||||
};
|
||||
|
||||
handleTranslate = () => {
|
||||
this.props.onTranslate(this.props.status);
|
||||
};
|
||||
@@ -525,16 +437,10 @@ class Status extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
|
||||
|
||||
const {
|
||||
parseClick,
|
||||
setCollapsed,
|
||||
} = this;
|
||||
|
||||
const {
|
||||
status,
|
||||
account,
|
||||
settings,
|
||||
collapsed,
|
||||
muted,
|
||||
intersectionObserverWrapper,
|
||||
onOpenVideo,
|
||||
@@ -543,7 +449,6 @@ class Status extends ImmutablePureComponent {
|
||||
history,
|
||||
...other
|
||||
} = this.props;
|
||||
const { isCollapsed } = this.state;
|
||||
let attachments = null;
|
||||
|
||||
// Depending on user settings, some media are considered as parts of the
|
||||
@@ -555,6 +460,7 @@ class Status extends ImmutablePureComponent {
|
||||
let extraMediaIcons = [];
|
||||
let media = contentMedia;
|
||||
let mediaIcons = contentMediaIcons;
|
||||
let statusAvatar;
|
||||
|
||||
if (settings.getIn(['content_warnings', 'media_outside'])) {
|
||||
media = extraMedia;
|
||||
@@ -578,7 +484,6 @@ class Status extends ImmutablePureComponent {
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleExpandedToggle,
|
||||
bookmark: this.handleHotkeyBookmark,
|
||||
toggleCollapse: this.handleHotkeyCollapse,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
openMedia: this.handleHotkeyOpenMedia,
|
||||
};
|
||||
@@ -650,7 +555,7 @@ class Status extends ImmutablePureComponent {
|
||||
sensitive={status.get('sensitive')}
|
||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
|
||||
hidden={isCollapsed || !isExpanded}
|
||||
hidden={!isExpanded}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
@@ -707,7 +612,7 @@ class Status extends ImmutablePureComponent {
|
||||
sensitive={status.get('sensitive')}
|
||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
|
||||
preventPlayback={isCollapsed || !isExpanded}
|
||||
preventPlayback={!isExpanded}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||
visible={this.state.showMedia}
|
||||
@@ -754,15 +659,8 @@ class Status extends ImmutablePureComponent {
|
||||
<StatusPrepend
|
||||
type={this.props.prepend}
|
||||
account={account}
|
||||
parseClick={parseClick}
|
||||
notificationId={this.props.notificationId}
|
||||
>
|
||||
{muted && settings.getIn(['collapsed', 'enabled']) && (
|
||||
<div className='notification__message-collapse-button'>
|
||||
<CollapseButton collapsed={isCollapsed} setCollapsed={setCollapsed} />
|
||||
</div>
|
||||
)}
|
||||
</StatusPrepend>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -770,13 +668,19 @@ class Status extends ImmutablePureComponent {
|
||||
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
|
||||
}
|
||||
|
||||
if (account === undefined || account === null) {
|
||||
statusAvatar = <Avatar account={status.get('account')} size={avatarSize} />;
|
||||
} else {
|
||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
contentMedia.push(hashtagBar);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<div
|
||||
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })}
|
||||
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread })}
|
||||
{...selectorAttribs}
|
||||
tabIndex={unfocusable ? null : 0}
|
||||
data-featured={featured ? 'true' : null}
|
||||
@@ -792,50 +696,47 @@ class Status extends ImmutablePureComponent {
|
||||
>
|
||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||
|
||||
{(!muted || !isCollapsed) && (
|
||||
{(!muted) && (
|
||||
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */
|
||||
<header onClick={this.parseClick} className='status__info'>
|
||||
<StatusHeader
|
||||
status={status}
|
||||
friend={account}
|
||||
collapsed={isCollapsed}
|
||||
parseClick={parseClick}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
<header onClick={this.handleClick} className='status__info'>
|
||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
<StatusIcons
|
||||
status={status}
|
||||
mediaIcons={contentMediaIcons.concat(extraMediaIcons)}
|
||||
collapsible={!muted && settings.getIn(['collapsed', 'enabled'])}
|
||||
collapsed={isCollapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
settings={settings.get('status_icons')}
|
||||
/>
|
||||
</header>
|
||||
)}
|
||||
<StatusContent
|
||||
status={status}
|
||||
onClick={this.handleClick}
|
||||
onTranslate={this.handleTranslate}
|
||||
collapsible
|
||||
media={contentMedia}
|
||||
extraMedia={extraMedia}
|
||||
mediaIcons={contentMediaIcons}
|
||||
expanded={isExpanded}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
onTranslate={this.handleTranslate}
|
||||
parseClick={parseClick}
|
||||
disabled={!history}
|
||||
onCollapsedToggle={this.handleCollapsedToggle}
|
||||
tagLinks={settings.get('tag_misleading_links')}
|
||||
rewriteMentions={settings.get('rewrite_mentions')}
|
||||
{...statusContentProps}
|
||||
/>
|
||||
|
||||
{(!isCollapsed || !(muted || !settings.getIn(['collapsed', 'show_action_bar']))) && (
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={status.get('account')}
|
||||
showReplyCount={settings.get('show_reply_count')}
|
||||
onFilter={matchedFilters ? this.handleFilterClick : null}
|
||||
{...other}
|
||||
/>
|
||||
)}
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={status.get('account')}
|
||||
showReplyCount={settings.get('show_reply_count')}
|
||||
onFilter={matchedFilters ? this.handleFilterClick : null}
|
||||
{...other}
|
||||
/>
|
||||
|
||||
{notification && (
|
||||
<NotificationOverlayContainer
|
||||
notification={notification}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { withRouter } from 'react-router-dom';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import ImageIcon from '@/material-icons/400-24px/image.svg?react';
|
||||
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
|
||||
@@ -20,9 +21,10 @@ import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity
|
||||
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
|
||||
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
|
||||
|
||||
|
||||
import { Permalink } from './permalink';
|
||||
|
||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||
|
||||
const textMatchesTarget = (text, origin, host) => {
|
||||
return (text === origin || text === host
|
||||
|| text.startsWith(origin + '/') || text.startsWith(host + '/')
|
||||
@@ -134,14 +136,14 @@ class StatusContent extends PureComponent {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
statusContent: PropTypes.string,
|
||||
expanded: PropTypes.bool,
|
||||
collapsed: PropTypes.bool,
|
||||
onExpandedToggle: PropTypes.func,
|
||||
onTranslate: PropTypes.func,
|
||||
media: PropTypes.node,
|
||||
extraMedia: PropTypes.node,
|
||||
mediaIcons: PropTypes.arrayOf(PropTypes.string),
|
||||
parseClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
collapsible: PropTypes.bool,
|
||||
onCollapsedToggle: PropTypes.func,
|
||||
onUpdate: PropTypes.func,
|
||||
tagLinks: PropTypes.bool,
|
||||
rewriteMentions: PropTypes.string,
|
||||
@@ -170,16 +172,21 @@ class StatusContent extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status, onCollapsedToggle } = this.props;
|
||||
const links = node.querySelectorAll('a');
|
||||
|
||||
let link, mention;
|
||||
|
||||
for (var i = 0; i < links.length; ++i) {
|
||||
let link = links[i];
|
||||
link = links[i];
|
||||
|
||||
if (link.classList.contains('status-link')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
link.classList.add('status-link');
|
||||
|
||||
let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
||||
mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
||||
|
||||
if (mention) {
|
||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||
@@ -195,7 +202,6 @@ class StatusContent extends PureComponent {
|
||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||
} else {
|
||||
link.addEventListener('click', this.onLinkClick.bind(this), false);
|
||||
link.setAttribute('title', link.href);
|
||||
link.classList.add('unhandled-link');
|
||||
|
||||
@@ -228,6 +234,18 @@ class StatusContent extends PureComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status.get('collapsed', null) === null && onCollapsedToggle) {
|
||||
const { collapsible, onClick } = this.props;
|
||||
|
||||
const collapsed =
|
||||
collapsible
|
||||
&& onClick
|
||||
&& node.clientHeight > MAX_HEIGHT
|
||||
&& status.get('spoiler_text').length === 0;
|
||||
|
||||
onCollapsedToggle(collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseEnter = ({ currentTarget }) => {
|
||||
@@ -265,23 +283,19 @@ class StatusContent extends PureComponent {
|
||||
if (this.props.onUpdate) this.props.onUpdate();
|
||||
}
|
||||
|
||||
onLinkClick = (e) => {
|
||||
if (this.props.collapsed) {
|
||||
if (this.props.parseClick) this.props.parseClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
onMentionClick = (mention, e) => {
|
||||
if (this.props.parseClick) {
|
||||
this.props.parseClick(e, `/@${mention.get('acct')}`);
|
||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.props.history.push(`/@${mention.get('acct')}`);
|
||||
}
|
||||
};
|
||||
|
||||
onHashtagClick = (hashtag, e) => {
|
||||
hashtag = hashtag.replace(/^#/, '');
|
||||
|
||||
if (this.props.parseClick) {
|
||||
this.props.parseClick(e, `/tags/${hashtag}`);
|
||||
if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.props.history.push(`/tags/${hashtag}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -290,9 +304,7 @@ class StatusContent extends PureComponent {
|
||||
};
|
||||
|
||||
handleMouseUp = (e) => {
|
||||
const { parseClick, disabled } = this.props;
|
||||
|
||||
if (disabled || !this.startXY) {
|
||||
if (!this.startXY) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -307,8 +319,8 @@ class StatusContent extends PureComponent {
|
||||
element = element.parentNode;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && e.button === 0 && parseClick) {
|
||||
parseClick(e);
|
||||
if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
|
||||
this.startXY = null;
|
||||
@@ -338,14 +350,13 @@ class StatusContent extends PureComponent {
|
||||
media,
|
||||
extraMedia,
|
||||
mediaIcons,
|
||||
parseClick,
|
||||
disabled,
|
||||
tagLinks,
|
||||
rewriteMentions,
|
||||
intl,
|
||||
statusContent,
|
||||
} = this.props;
|
||||
|
||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
||||
@@ -355,10 +366,17 @@ class StatusContent extends PureComponent {
|
||||
const spoilerHtml = status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml');
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
const classNames = classnames('status__content', {
|
||||
'status__content--with-action': parseClick && !disabled,
|
||||
'status__content--with-action': this.props.onClick && this.props.history,
|
||||
'status__content--collapsed': renderReadMore,
|
||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||
});
|
||||
|
||||
const readMoreButton = renderReadMore && (
|
||||
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
|
||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' icon={ChevronRightIcon} />
|
||||
</button>
|
||||
);
|
||||
|
||||
const translateButton = renderTranslate && (
|
||||
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} />
|
||||
);
|
||||
@@ -427,7 +445,7 @@ class StatusContent extends PureComponent {
|
||||
{extraMedia}
|
||||
</div>
|
||||
);
|
||||
} else if (parseClick) {
|
||||
} else if (this.props.onClick) {
|
||||
return (
|
||||
<div
|
||||
className={classNames}
|
||||
@@ -446,6 +464,7 @@ class StatusContent extends PureComponent {
|
||||
lang={language}
|
||||
/>
|
||||
{translateButton}
|
||||
{readMoreButton}
|
||||
{media}
|
||||
{extraMedia}
|
||||
</div>
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
// Mastodon imports.
|
||||
import { Avatar } from './avatar';
|
||||
import { AvatarOverlay } from './avatar_overlay';
|
||||
import { DisplayName } from './display_name';
|
||||
|
||||
export default class StatusHeader extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
friend: ImmutablePropTypes.map,
|
||||
avatarSize: PropTypes.number,
|
||||
parseClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
const { status, parseClick } = this.props;
|
||||
parseClick(e, `/@${status.getIn(['account', 'acct'])}`);
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
status,
|
||||
friend,
|
||||
avatarSize,
|
||||
} = this.props;
|
||||
|
||||
const account = status.get('account');
|
||||
|
||||
let statusAvatar;
|
||||
if (friend === undefined || friend === null) {
|
||||
statusAvatar = <Avatar account={account} size={avatarSize} />;
|
||||
} else {
|
||||
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={account.get('url')}
|
||||
className='status__display-name'
|
||||
target='_blank'
|
||||
onClick={this.handleAccountClick}
|
||||
rel='noopener noreferrer'
|
||||
title={status.getIn(['account', 'acct'])}
|
||||
data-hover-card-account={status.getIn(['account', 'id'])}
|
||||
>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
<DisplayName account={account} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,12 +16,9 @@ import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { languages } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { CollapseButton } from './collapse_button';
|
||||
import { VisibilityIcon } from './visibility_icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
collapse: { id: 'status.collapse', defaultMessage: 'Collapse' },
|
||||
uncollapse: { id: 'status.uncollapse', defaultMessage: 'Uncollapse' },
|
||||
inReplyTo: { id: 'status.in_reply_to', defaultMessage: 'This toot is a reply' },
|
||||
previewCard: { id: 'status.has_preview_card', defaultMessage: 'Features an attached preview card' },
|
||||
pictures: { id: 'status.has_pictures', defaultMessage: 'Features attached pictures' },
|
||||
@@ -53,22 +50,10 @@ class StatusIcons extends PureComponent {
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
mediaIcons: PropTypes.arrayOf(PropTypes.string),
|
||||
collapsible: PropTypes.bool,
|
||||
collapsed: PropTypes.bool,
|
||||
setCollapsed: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
// Handles clicks on collapsed button
|
||||
handleCollapsedClick = (e) => {
|
||||
const { collapsed, setCollapsed } = this.props;
|
||||
if (e.button === 0) {
|
||||
setCollapsed(!collapsed);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
renderIcon (mediaIcon) {
|
||||
const { intl } = this.props;
|
||||
|
||||
@@ -114,9 +99,6 @@ class StatusIcons extends PureComponent {
|
||||
const {
|
||||
status,
|
||||
mediaIcons,
|
||||
collapsible,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
settings,
|
||||
intl,
|
||||
} = this.props;
|
||||
@@ -142,7 +124,6 @@ class StatusIcons extends PureComponent {
|
||||
/>}
|
||||
{settings.get('media') && !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon))}
|
||||
{settings.get('visibility') && <VisibilityIcon visibility={status.get('visibility')} />}
|
||||
{collapsible && <CollapseButton collapsed={collapsed} setCollapsed={setCollapsed} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,27 +15,23 @@ import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { Permalink } from './permalink';
|
||||
|
||||
export default class StatusPrepend extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
parseClick: PropTypes.func.isRequired,
|
||||
notificationId: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
handleClick = (e) => {
|
||||
const { account, parseClick } = this.props;
|
||||
parseClick(e, `/@${account.get('acct')}`);
|
||||
};
|
||||
|
||||
Message = () => {
|
||||
const { type, account } = this.props;
|
||||
let link = (
|
||||
<a
|
||||
<Permalink
|
||||
onClick={this.handleClick}
|
||||
to={`/@${account.get('acct')}`}
|
||||
href={account.get('url')}
|
||||
className='status__display-name'
|
||||
data-hover-card-account={account.get('id')}
|
||||
@@ -47,7 +43,7 @@ export default class StatusPrepend extends PureComponent {
|
||||
}}
|
||||
/>
|
||||
</bdi>
|
||||
</a>
|
||||
</Permalink>
|
||||
);
|
||||
switch (type) {
|
||||
case 'featured':
|
||||
|
||||
Reference in new issue
Block a user