Merge branch 'emoji-reactions-base' into develop

# Conflicts:
#	app/javascript/flavours/glitch/components/status.jsx
#	app/javascript/flavours/glitch/locales/en.json
#	app/javascript/flavours/glitch/styles/components.scss
This commit is contained in:
Jeremy Kescher 2024-02-24 11:06:43 +01:00
commit 004b22602b
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
27 changed files with 804 additions and 965 deletions

View file

@ -43,4 +43,5 @@ export interface ApiAccountJSON {
suspended?: boolean;
limited?: boolean;
memorial?: boolean;
hide_collections: boolean;
}

View file

@ -147,7 +147,7 @@ class Account extends ImmutablePureComponent {
</div>
<div className='account__contents'>
<DisplayName account={account} inline />
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
{account.get('followers_count') !== -1 && (

View file

@ -0,0 +1,45 @@
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();
}
}, [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,
};

View file

@ -1,7 +1,5 @@
import React from 'react';
import classNames from 'classnames';
import type { List } from 'immutable';
import type { Account } from 'flavours/glitch/models/account';
@ -14,7 +12,6 @@ interface Props {
account?: Account;
others?: List<Account>;
localDomain?: string;
inline?: boolean;
}
export class DisplayName extends React.PureComponent<Props> {
@ -51,7 +48,7 @@ export class DisplayName extends React.PureComponent<Props> {
};
render() {
const { others, localDomain, inline } = this.props;
const { others, localDomain } = this.props;
let displayName: React.ReactNode,
suffix: React.ReactNode,
@ -114,13 +111,11 @@ export class DisplayName extends React.PureComponent<Props> {
return (
<span
className={classNames('display-name', { inline })}
className='display-name'
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{displayName}
{inline ? ' ' : null}
{suffix}
{displayName} {suffix}
</span>
);
}

View file

@ -18,26 +18,7 @@ import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
import { IconButton } from './icon_button';
const messages = defineMessages({
hidden: {
defaultMessage: 'Media hidden',
id: 'status.media_hidden',
},
sensitive: {
defaultMessage: 'Sensitive',
id: 'media_gallery.sensitive',
},
toggle: {
defaultMessage: 'Click to view',
id: 'status.sensitive_toggle',
},
toggle_visible: {
defaultMessage: '{number, plural, one {Hide image} other {Hide images}}',
id: 'media_gallery.toggle_visible',
},
warning: {
defaultMessage: 'Sensitive content',
id: 'status.sensitive_warning',
},
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
});
class Item extends PureComponent {
@ -299,8 +280,8 @@ class MediaGallery extends PureComponent {
this.props.onOpenMedia(this.props.media, index, this.props.lang);
};
handleRef = (node) => {
this.node = node;
handleRef = c => {
this.node = c;
if (this.node) {
this._setDimensions();
@ -379,11 +360,6 @@ class MediaGallery extends PureComponent {
<div className={computedClass} style={style} ref={this.handleRef}>
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
{spoilerButton}
{visible && sensitive && (
<span className='sensitive-marker'>
<FormattedMessage {...messages.sensitive} />
</span>
)}
</div>
{children}

View file

@ -23,6 +23,7 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components
import { displayMedia, visibleReactions } from '../initial_state';
import AttachmentList from './attachment_list';
import { CollapseButton } from './collapse_button';
import { getHashtagBarForStatus } from './hashtag_bar';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
@ -517,7 +518,6 @@ class Status extends ImmutablePureComponent {
render () {
const {
handleRef,
parseClick,
setCollapsed,
} = this;
@ -736,7 +736,6 @@ class Status extends ImmutablePureComponent {
<Card
onOpenMedia={this.handleOpenMedia}
card={status.get('card')}
compact
sensitive={status.get('sensitive')}
/>,
);
@ -772,7 +771,13 @@ class Status extends ImmutablePureComponent {
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>
);
}
@ -780,94 +785,86 @@ class Status extends ImmutablePureComponent {
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
}
const computedClass = classNames('status', `status-${status.get('visibility')}`, {
collapsed: isCollapsed,
'has-background': isCollapsed && background,
'status__wrapper-reply': !!status.get('in_reply_to_id'),
'status--in-thread': !!rootId,
'status--first-in-thread': previousId && (!connectUp || connectToRoot),
unread,
muted,
}, 'focusable');
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
contentMedia.push(hashtagBar);
return (
<HotKeys handlers={handlers}>
<div
className={computedClass}
style={isCollapsed && background ? { backgroundImage: `url(${background})` } : null}
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })}
{...selectorAttribs}
ref={handleRef}
tabIndex={0}
data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
ref={this.handleRef}
data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
>
{!muted && prepend}
{prepend}
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
<div
className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'has-background': isCollapsed && background, collapsed: isCollapsed })}
data-id={status.get('id')}
style={isCollapsed && background ? { backgroundImage: `url(${background})` } : null}
>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
<header className='status__info'>
<span>
{muted && prepend}
{!muted || !isCollapsed ? (
{(!muted || !isCollapsed) && (
<header className='status__info'>
<StatusHeader
status={status}
friend={account}
collapsed={isCollapsed}
parseClick={parseClick}
/>
) : null}
</span>
<StatusIcons
<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}
mediaIcons={contentMediaIcons.concat(extraMediaIcons)}
collapsible={settings.getIn(['collapsed', 'enabled'])}
collapsed={isCollapsed}
setCollapsed={setCollapsed}
settings={settings.get('status_icons')}
media={contentMedia}
extraMedia={extraMedia}
mediaIcons={contentMediaIcons}
expanded={isExpanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
parseClick={parseClick}
disabled={!history}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>
</header>
<StatusContent
status={status}
media={contentMedia}
extraMedia={extraMedia}
mediaIcons={contentMediaIcons}
expanded={isExpanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
parseClick={parseClick}
disabled={!history}
tagLinks={settings.get('tag_misleading_links')}
rewriteMentions={settings.get('rewrite_mentions')}
{...statusContentProps}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/>
<StatusReactions
statusId={status.get('id')}
reactions={status.get('reactions')}
numVisible={visibleReactions}
addReaction={this.props.onReactionAdd}
removeReaction={this.props.onReactionRemove}
canReact={this.context.identity.signedIn}
/>
{!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}
/>
) : null}
{notification ? (
<NotificationOverlayContainer
notification={notification}
/>
) : null}
{(!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}
/>
)}
{notification && (
<NotificationOverlayContainer
notification={notification}
/>
)}
</div>
</div>
</HotKeys>
);

View file

@ -45,26 +45,19 @@ export default class StatusHeader extends PureComponent {
}
return (
<div className='status__info__account'>
<a
href={account.get('url')}
target='_blank'
className='status__avatar'
onClick={this.handleAccountClick}
rel='noopener noreferrer'
>
<a
href={account.get('url')}
className='status__display-name'
target='_blank'
onClick={this.handleAccountClick}
rel='noopener noreferrer'
>
<div className='status__avatar'>
{statusAvatar}
</a>
<a
href={account.get('url')}
target='_blank'
className='status__display-name'
onClick={this.handleAccountClick}
rel='noopener noreferrer'
>
<DisplayName account={account} />
</a>
</div>
</div>
<DisplayName account={account} />
</a>
);
}

View file

@ -6,7 +6,6 @@ import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ExpandLessIcon from '@/material-icons/400-24px/expand_less.svg?react';
import ForumIcon from '@/material-icons/400-24px/forum.svg?react';
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
import ImageIcon from '@/material-icons/400-24px/image.svg?react';
@ -17,8 +16,7 @@ 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 { IconButton } from './icon_button';
import { CollapseButton } from './collapse_button';
import { VisibilityIcon } from './visibility_icon';
const messages = defineMessages({
@ -118,6 +116,7 @@ class StatusIcons extends PureComponent {
mediaIcons,
collapsible,
collapsed,
setCollapsed,
settings,
intl,
} = this.props;
@ -143,21 +142,7 @@ class StatusIcons extends PureComponent {
/>}
{settings.get('media') && !!mediaIcons && mediaIcons.map(icon => this.renderIcon(icon))}
{settings.get('visibility') && <VisibilityIcon visibility={status.get('visibility')} />}
{collapsible && (
<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={this.handleCollapsedClick}
/>
)}
{collapsible && <CollapseButton collapsed={collapsed} setCollapsed={setCollapsed} />}
</div>
);
}

View file

@ -24,6 +24,7 @@ export default class StatusPrepend extends PureComponent {
account: ImmutablePropTypes.map.isRequired,
parseClick: PropTypes.func.isRequired,
notificationId: PropTypes.number,
children: PropTypes.node,
};
handleClick = (e) => {
@ -39,11 +40,13 @@ export default class StatusPrepend extends PureComponent {
href={account.get('url')}
className='status__display-name'
>
<b
dangerouslySetInnerHTML={{
__html : account.get('display_name_html') || account.get('username'),
}}
/>
<bdi>
<strong
dangerouslySetInnerHTML={{
__html : account.get('display_name_html') || account.get('username'),
}}
/>
</bdi>
</a>
);
switch (type) {
@ -121,7 +124,7 @@ export default class StatusPrepend extends PureComponent {
render () {
const { Message } = this;
const { type } = this.props;
const { type, children } = this.props;
let iconId, iconComponent;
@ -159,14 +162,13 @@ export default class StatusPrepend extends PureComponent {
return !type ? null : (
<aside className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend' : 'notification__message'}>
<div className={type === 'reblogged_by' || type === 'featured' ? 'status__prepend-icon-wrapper' : 'notification__favourite-icon-wrapper'}>
<Icon
className={`status__prepend-icon ${type === 'favourite' ? 'star-icon' : ''}`}
id={iconId}
icon={iconComponent}
/>
</div>
<Icon
className={`status__prepend-icon ${type === 'favourite' ? 'star-icon' : ''}`}
id={iconId}
icon={iconComponent}
/>
<Message />
{children}
</aside>
);
}

View file

@ -1,54 +1,38 @@
import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import TripIcon from '@/material-icons/400-24px/trip.svg?react';
import { Icon } from 'flavours/glitch/components/icon';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { AvatarOverlay } from '../../../components/avatar_overlay';
import { DisplayName } from '../../../components/display_name';
import { Permalink } from '../../../components/permalink';
class MovedNote extends ImmutablePureComponent {
export default class MovedNote extends ImmutablePureComponent {
static propTypes = {
from: ImmutablePropTypes.map.isRequired,
to: ImmutablePropTypes.map.isRequired,
...WithRouterPropTypes,
};
handleAccountClick = e => {
if (e.button === 0) {
e.preventDefault();
this.props.history.push(`/@${this.props.to.get('acct')}`);
}
e.stopPropagation();
};
render () {
const { from, to } = this.props;
const displayNameHtml = { __html: from.get('display_name_html') };
return (
<div className='account__moved-note'>
<div className='account__moved-note__message'>
<div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' icon={TripIcon} /></div>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
<div className='moved-account-banner'>
<div className='moved-account-banner__message'>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: from.get('display_name_html') }} /></bdi> }} />
</div>
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
<DisplayName account={to} />
</a>
<div className='moved-account-banner__action'>
<Permalink to={`/@${to.get('acct')}`} href={to.get('url')} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
<DisplayName account={to} />
</Permalink>
<Permalink to={`/@${to.get('acct')}`} href={to.get('url')} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Permalink>
</div>
</div>
);
}
}
export default withRouter(MovedNote);

View file

@ -156,7 +156,11 @@ export default class ComposerOptionsDropdownContent extends PureComponent {
if (!contents) {
contents = (
<>
{icon && <Icon className='icon' id={icon} icon={iconComponent} />}
{icon && (
<div className='privacy-dropdown__option__icon'>
<Icon className='icon' id={icon} icon={iconComponent} />
</div>
)}
<div className='privacy-dropdown__option__content'>
<strong>{text}</strong>

View file

@ -110,7 +110,9 @@ class ToggleOptionImpl extends ImmutablePureComponent {
return (
<>
<Toggle checked={checked} onChange={this.handleChange} />
<div className='privacy-dropdown__option__icon'>
<Toggle checked={checked} onChange={this.handleChange} />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{text}</strong>

View file

@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId),
};
};
@ -117,7 +118,7 @@ class Followers extends ImmutablePureComponent {
};
render () {
const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) {
return (
@ -141,6 +142,8 @@ class Followers extends ImmutablePureComponent {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {

View file

@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId),
};
};
@ -117,7 +118,7 @@ class Following extends ImmutablePureComponent {
};
render () {
const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props;
const { accountId, accountIds, hasMore, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) {
return (
@ -141,6 +142,8 @@ class Following extends ImmutablePureComponent {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />;
} else {

View file

@ -95,9 +95,7 @@ class AdminReport extends ImmutablePureComponent {
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex={0}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='flag' icon={FlagIcon} />
</div>
<Icon id='flag' icon={FlagIcon} />
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.admin.report' defaultMessage='{name} reported {target}' values={{ name: link, target: targetLink }} />

View file

@ -86,9 +86,7 @@ class NotificationAdminSignup extends ImmutablePureComponent {
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex={0}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user-plus' icon={PersonAddIcon} />
</div>
<Icon id='user-plus' icon={PersonAddIcon} />
<FormattedMessage
id='notification.admin.sign_up'

View file

@ -86,9 +86,7 @@ class NotificationFollow extends ImmutablePureComponent {
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex={0}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user-plus' icon={PersonAddIcon} />
</div>
<Icon id='user-plus' icon={PersonAddIcon} />
<FormattedMessage
id='notification.follow'

View file

@ -108,9 +108,7 @@ class FollowRequest extends ImmutablePureComponent {
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex={0}>
<div className='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='user' icon={PersonIcon} />
</div>
<Icon id='user' icon={PersonIcon} />
<FormattedMessage
id='notification.follow_request'

View file

@ -12,7 +12,6 @@ export default class SettingToggle extends PureComponent {
settings: ImmutablePropTypes.map.isRequired,
settingPath: PropTypes.array.isRequired,
label: PropTypes.node.isRequired,
meta: PropTypes.node,
onChange: PropTypes.func.isRequired,
defaultValue: PropTypes.bool,
disabled: PropTypes.bool,
@ -23,14 +22,13 @@ export default class SettingToggle extends PureComponent {
};
render () {
const { prefix, settings, settingPath, label, meta, defaultValue, disabled } = this.props;
const { prefix, settings, settingPath, label, defaultValue, disabled } = this.props;
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
<Toggle disabled={disabled} id={id} checked={settings.getIn(settingPath, defaultValue)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>}
</div>
);
}

View file

@ -8,12 +8,12 @@ import classNames from 'classnames';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import { useBlurhash } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
@ -51,14 +51,9 @@ export default class Card extends PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
sensitive: PropTypes.bool,
};
static defaultProps = {
compact: false,
};
state = {
previewLoaded: false,
embedded: false,
@ -69,6 +64,7 @@ export default class Card extends PureComponent {
if (!Immutable.is(this.props.card, nextProps.card)) {
this.setState({ embedded: false, previewLoaded: false });
}
if (this.props.sensitive !== nextProps.sensitive) {
this.setState({ revealed: !nextProps.sensitive });
}
@ -82,35 +78,8 @@ export default class Card extends PureComponent {
window.removeEventListener('resize', this.handleResize);
}
handlePhotoClick = () => {
const { card, onOpenMedia } = this.props;
onOpenMedia(
Immutable.fromJS([
{
type: 'image',
url: card.get('embed_url'),
description: card.get('title'),
meta: {
original: {
width: card.get('width'),
height: card.get('height'),
},
},
},
]),
0,
);
};
handleEmbedClick = () => {
const { card } = this.props;
if (card.get('type') === 'photo') {
this.handlePhotoClick();
} else {
this.setState({ embedded: true });
}
this.setState({ embedded: true });
};
setRef = c => {
@ -128,21 +97,21 @@ export default class Card extends PureComponent {
};
renderVideo () {
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
const { card } = this.props;
const content = { __html: addAutoPlay(card.get('html')) };
return (
<div
ref={this.setRef}
className='status-card__image status-card-video'
dangerouslySetInnerHTML={content}
style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
style={{ aspectRatio: '16 / 9' }}
/>
);
}
render () {
const { card, compact } = this.props;
const { card } = this.props;
const { embedded, revealed } = this.state;
if (card === null) {
@ -150,29 +119,37 @@ export default class Card extends PureComponent {
}
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link';
const className = classNames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const interactive = card.get('type') === 'video';
const language = card.get('language') || '';
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
const description = (
<div className='status-card__content' lang={language}>
{title}
{!(horizontal || compact) && <p className='status-card__description' title={card.get('description')}>{card.get('description')}</p>}
<span className='status-card__host'>{provider}</span>
<div className='status-card__content'>
<span className='status-card__host'>
<span lang={language}>{provider}</span>
{card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}
</span>
<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
{card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>}
</div>
);
const thumbnailStyle = {
visibility: revealed? null : 'hidden',
visibility: revealed ? null : 'hidden',
};
if (horizontal) {
thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
if (largeImage && card.get('type') === 'video') {
thumbnailStyle.aspectRatio = `16 / 9`;
} else if (largeImage) {
thumbnailStyle.aspectRatio = '1.91 / 1';
} else {
thumbnailStyle.aspectRatio = 1;
}
let embed = '';
let embed;
let canvas = (
<Blurhash
className={classNames('status-card__image-preview', {
@ -205,22 +182,16 @@ export default class Card extends PureComponent {
if (embedded) {
embed = this.renderVideo();
} else {
let iconVariant = 'play';
if (card.get('type') === 'photo') {
iconVariant = 'search-plus';
}
embed = (
<div className='status-card__image'>
{canvas}
{thumbnail}
{revealed ? (
<div className='status-card__actions'>
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
<div>
<button type='button' onClick={this.handleEmbedClick}><Icon id={iconVariant} icon={PlayArrowIcon} /></button>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a>}
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button>
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' icon={OpenInNewIcon} /></a>
</div>
</div>
) : spoilerButton}
@ -229,9 +200,9 @@ export default class Card extends PureComponent {
}
return (
<div className={className} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
<div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
{embed}
{!compact && description}
<a href={card.get('url')} target='_blank' rel='noopener noreferrer'>{description}</a>
</div>
);
} else if (card.get('image')) {
@ -250,7 +221,7 @@ export default class Card extends PureComponent {
}
return (
<a href={card.get('url')} className={className} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
{embed}
{description}
</a>

View file

@ -5,11 +5,6 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'flavours/glitch/components/avatar';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
import StatusContent from 'flavours/glitch/components/status_content';
import { IconButton } from '../../../components/icon_button';
export default class ActionsModal extends ImmutablePureComponent {
@ -58,33 +53,9 @@ export default class ActionsModal extends ImmutablePureComponent {
};
render () {
const status = this.props.status && (
<div className='status light'>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={this.props.status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<RelativeTimestamp timestamp={this.props.status.get('created_at')} />
</a>
</div>
<a href={this.props.status.getIn(['account', 'url'])} className='status__display-name' rel='noopener noreferrer'>
<div className='status__avatar'>
<Avatar account={this.props.status.get('account')} size={48} />
</div>
<DisplayName account={this.props.status.get('account')} />
</a>
</div>
<StatusContent status={this.props.status} />
</div>
);
return (
<div className='modal-root__modal actions-modal'>
{status}
<ul className={classNames({ 'with-status': !!status })}>
<ul>
{this.props.actions.map(this.renderAction)}
</ul>
</div>

View file

@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { changeBoostPrivacy } from 'flavours/glitch/actions/boosts';
import AttachmentList from 'flavours/glitch/components/attachment_list';
@ -78,12 +77,11 @@ class BoostModal extends ImmutablePureComponent {
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<VisibilityIcon visibility={status.get('visibility')} />
<RelativeTimestamp timestamp={status.get('created_at')} /></a>
</div>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>

View file

@ -8,7 +8,6 @@ import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import AttachmentList from 'flavours/glitch/components/attachment_list';
import { Avatar } from 'flavours/glitch/components/avatar';
@ -54,13 +53,11 @@ class FavouriteModal extends ImmutablePureComponent {
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
<div className='boost-modal__status-header'>
<div className='boost-modal__status-time'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<VisibilityIcon visibility={status.get('visibility')} />
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
</div>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
<div className='status__avatar'>
@ -68,7 +65,6 @@ class FavouriteModal extends ImmutablePureComponent {
</div>
<DisplayName account={status.get('account')} />
</a>
</div>

View file

@ -52,7 +52,6 @@
"keyboard_shortcuts.bookmark": "to bookmark",
"keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting",
"keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots",
"media_gallery.sensitive": "Sensitive",
"moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.",
"navigation_bar.app_settings": "App settings",
"navigation_bar.featured_users": "Featured users",
@ -157,7 +156,6 @@
"status.is_poll": "This toot is a poll",
"status.local_only": "Only visible from your instance",
"status.react": "React",
"status.sensitive_toggle": "Click to view",
"status.uncollapse": "Uncollapse",
"suggestions.dismiss": "Dismiss suggestion"
}

View file

@ -94,6 +94,7 @@ export const accountDefaultValues: AccountShape = {
memorial: false,
limited: false,
moved: null,
hide_collections: false,
};
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);

File diff suppressed because it is too large Load diff

View file

@ -767,7 +767,7 @@ const startServer = async () => {
// Only send local-only statuses to logged-in users
if ((event === 'update' || event === 'status.update') && payload.local_only && !(req.accountId && allowLocalOnly)) {
log.silly(req.requestId, `Message ${payload.id} filtered because it was local-only`);
log.debug(`Message ${payload.id} filtered because it was local-only`);
return;
}