Merge branch 'next_' into 'next'

next

See merge request soapbox-pub/soapbox-fe!1196
next-virtuoso-proof
Alex Gleason 2 years ago
commit b69f12748f

@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestAccount from '../features/compose/components/autosuggest_account';
import { isRtl } from '../rtl';
import AutosuggestEmoji from './autosuggest_emoji';
@ -205,7 +205,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
inner = <AutosuggestAccount id={suggestion} />;
key = suggestion;
}

@ -602,7 +602,7 @@ class MediaGallery extends React.PureComponent {
/>
) : (
<button type='button' onClick={this.handleOpen} className='bg-transparent w-full h-full border-0'>
<div className='p-4 rounded-xl shadow-xl backdrop-blur-sm bg-white/75 text-center inline-block space-y-4 max-w-[280px]'>
<div className='p-4 rounded-xl shadow-xl backdrop-blur-sm bg-white/75 dark:bg-slate-800/75 text-center inline-block space-y-4 max-w-[280px]'>
<div className='space-y-1'>
<Text weight='semibold'>{warning}</Text>
<Text size='sm'>

@ -429,7 +429,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
defaultMessage='{name} reposted'
values={{
name: <bdi>
<strong className='text-gray-800' dangerouslySetInnerHTML={displayNameHtml} />
<strong className='text-gray-800 dark:text-gray-200' dangerouslySetInnerHTML={displayNameHtml} />
</bdi>,
}}
/>

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import * as React from 'react';
const Select = React.forwardRef((props, ref) => {
const { children, ...filteredProps } = props;
return (
<select
ref={ref}
className='pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md'
{...filteredProps}
>
{children}
</select>
);
});
Select.propTypes = {
children: PropTypes.node,
};
export default Select;

@ -0,0 +1,17 @@
import * as React from 'react';
const Select = React.forwardRef<HTMLSelectElement>((props, ref) => {
const { children, ...filteredProps } = props;
return (
<select
ref={ref}
className='pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-slate-800 sm:text-sm rounded-md'
{...filteredProps}
>
{children}
</select>
);
});
export default Select;

@ -1,20 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import Account from '../../../components/account';
import { makeGetAccount } from '../../../selectors';
const AutosuggestAccount = ({ id }) => {
const getAccount = makeGetAccount();
const account = useSelector((state) => getAccount(state, id));
return <Account account={account} hideActions showProfileHoverCard={false} />;
};
AutosuggestAccount.propTypes = {
id: PropTypes.number.isRequired,
};
export default AutosuggestAccount;

@ -0,0 +1,21 @@
import React from 'react';
import Account from 'soapbox/components/account';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
interface IAutosuggestAccount {
id: string,
}
const AutosuggestAccount: React.FC<IAutosuggestAccount> = ({ id }) => {
const getAccount = makeGetAccount();
const account = useAppSelector((state) => getAccount(state, id));
if (!account) return null;
return <Account account={account} hideActions showProfileHoverCard={false} />;
};
export default AutosuggestAccount;

@ -14,6 +14,8 @@ import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { Button } from '../../../components/ui';
import { isMobile } from '../../../is_mobile';
import ReplyMentions from '../components/reply_mentions';
import UploadForm from '../components/upload_form';
import Warning from '../components/warning';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import MarkdownButtonContainer from '../containers/markdown_button_container';
@ -22,12 +24,10 @@ import PollFormContainer from '../containers/poll_form_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import QuotedStatusContainer from '../containers/quoted_status_container';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import ReplyMentions from '../containers/reply_mentions_container';
import ScheduleButtonContainer from '../containers/schedule_button_container';
import ScheduleFormContainer from '../containers/schedule_form_container';
import SpoilerButtonContainer from '../containers/spoiler_button_container';
import UploadButtonContainer from '../containers/upload_button_container';
import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container';
import { countableText } from '../util/counter';
@ -354,7 +354,7 @@ class ComposeForm extends ImmutablePureComponent {
{
!condensed &&
<div className='compose-form__modifiers'>
<UploadFormContainer />
<UploadForm />
<PollFormContainer />
<ScheduleFormContainer />
</div>

@ -1,56 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl';
export default @injectIntl
class ReplyMentions extends ImmutablePureComponent {
static propTypes = {
onOpenMentionsModal: PropTypes.func.isRequired,
explicitAddressing: PropTypes.bool,
to: ImmutablePropTypes.orderedSet,
parentTo: ImmutablePropTypes.orderedSet,
isReply: PropTypes.bool,
};
handleClick = e => {
e.preventDefault();
this.props.onOpenMentionsModal();
}
render() {
const { explicitAddressing, to, parentTo, isReply } = this.props;
if (!explicitAddressing || !isReply || !to || (parentTo.size === 0)) {
return null;
}
if (to.size === 0) {
return (
<a href='#' className='reply-mentions' onClick={this.handleClick}>
<FormattedMessage
id='reply_mentions.reply_empty'
defaultMessage='Replying to post'
/>
</a>
);
}
return (
<a href='#' className='reply-mentions' onClick={this.handleClick}>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
values={{
accounts: to.slice(0, 2).map(acct => <><span className='reply-mentions__account'>@{acct.split('@')[0]}</span>{' '}</>),
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
}}
/>
</a>
);
}
}

@ -0,0 +1,64 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { openModal } from 'soapbox/actions/modals';
import { useAppSelector } from 'soapbox/hooks';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import type { Status as StatusEntity } from 'soapbox/types/entities';
const ReplyMentions: React.FC = () => {
const dispatch = useDispatch();
const instance = useAppSelector((state) => state.instance);
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') }));
const to = useAppSelector((state) => state.compose.get('to'));
const account = useAppSelector((state) => state.accounts.get(state.me));
const { explicitAddressing } = getFeatures(instance);
if (!explicitAddressing || !status || !to) {
return null;
}
const parentTo = status && statusToMentionsAccountIdsArray(status, account);
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
dispatch(openModal('REPLY_MENTIONS'));
};
if (!parentTo || (parentTo.size === 0)) {
return null;
}
if (to.size === 0) {
return (
<a href='#' className='reply-mentions' onClick={handleClick}>
<FormattedMessage
id='reply_mentions.reply_empty'
defaultMessage='Replying to post'
/>
</a>
);
}
return (
<a href='#' className='reply-mentions' onClick={handleClick}>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
values={{
accounts: to.slice(0, 2).map((acct: string) => <><span className='reply-mentions__account'>@{acct.split('@')[0]}</span>{' '}</>),
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
}}
/>
</a>
);
};
export default ReplyMentions;

@ -1,31 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { length } from 'stringz';
export default class TextCharacterCounter extends React.PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
max: PropTypes.number.isRequired,
};
checkRemainingText(diff) {
return (
<span
className={classNames('text-sm font-semibold', {
'text-gray-400': diff >= 0,
'text-danger-600': diff < 0,
})}
>
{diff}
</span>
);
}
render() {
const diff = this.props.max - length(this.props.text);
return this.checkRemainingText(diff);
}
}

@ -0,0 +1,28 @@
import classNames from 'classnames';
import React from 'react';
import { length } from 'stringz';
interface ITextCharacterCounter {
max: number,
text: string,
}
const TextCharacterCounter: React.FC<ITextCharacterCounter> = ({ text, max }) => {
const checkRemainingText = (diff: number) => {
return (
<span
className={classNames('text-sm font-semibold', {
'text-gray-400': diff >= 0,
'text-danger-600': diff < 0,
})}
>
{diff}
</span>
);
};
const diff = max - length(text);
return checkRemainingText(diff);
};
export default TextCharacterCounter;

@ -1,37 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
// import SensitiveButtonContainer from '../containers/sensitive_button_container';
import UploadProgress from '../components/upload-progress';
import UploadContainer from '../containers/upload_container';
export default class UploadForm extends ImmutablePureComponent {
static propTypes = {
mediaIds: ImmutablePropTypes.list.isRequired,
};
render() {
const { mediaIds } = this.props;
const classes = classNames('compose-form__uploads-wrapper', {
'contains-media': mediaIds.size !== 0,
});
return (
<div className='compose-form__upload-wrapper'>
<UploadProgress />
<div className={classes}>
{mediaIds.map(id => (
<UploadContainer id={id} key={id} />
))}
</div>
{/* {!mediaIds.isEmpty() && <SensitiveButtonContainer />} */}
</div>
);
}
}

@ -0,0 +1,33 @@
import classNames from 'classnames';
import React from 'react';
import { useAppSelector } from 'soapbox/hooks';
// import SensitiveButtonContainer from '../containers/sensitive_button_container';
import UploadProgress from '../components/upload-progress';
import UploadContainer from '../containers/upload_container';
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
const UploadForm = () => {
const mediaIds = useAppSelector((state) => state.compose.get('media_attachments').map((item: AttachmentEntity) => item.get('id')));
const classes = classNames('compose-form__uploads-wrapper', {
'contains-media': mediaIds.size !== 0,
});
return (
<div className='compose-form__upload-wrapper'>
<UploadProgress />
<div className={classes}>
{mediaIds.map((id: string) => (
<UploadContainer id={id} key={id} />
))}
</div>
{/* {!mediaIds.isEmpty() && <SensitiveButtonContainer />} */}
</div>
);
};
export default UploadForm;

@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import { length } from 'stringz';
import ProgressCircle from 'soapbox/components/progress_circle';
const messages = defineMessages({
title: { id: 'compose.character_counter.title', defaultMessage: 'Used {chars} out of {maxChars} characters' },
});
/**
* Renders a character counter
* @param {string} props.text - text to use to measure
* @param {number} props.max - max text allowed
*/
class VisualCharacterCounter extends React.PureComponent {
render() {
const { intl, text, max } = this.props;
const textLength = length(text);
const progress = textLength / max;
return (
<ProgressCircle
title={intl.formatMessage(messages.title, { chars: textLength, maxChars: max })}
progress={progress}
radius={10}
stroke={3}
/>
);
}
}
VisualCharacterCounter.propTypes = {
intl: PropTypes.object.isRequired,
text: PropTypes.string.isRequired,
max: PropTypes.number.isRequired,
};
export default injectIntl(VisualCharacterCounter);

@ -0,0 +1,35 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { length } from 'stringz';
import ProgressCircle from 'soapbox/components/progress_circle';
const messages = defineMessages({
title: { id: 'compose.character_counter.title', defaultMessage: 'Used {chars} out of {maxChars} characters' },
});
interface IVisualCharacterCounter {
/** max text allowed */
max: number,
/** text to use to measure */
text: string,
}
/** Renders a character counter */
const VisualCharacterCounter: React.FC<IVisualCharacterCounter> = ({ text, max }) => {
const intl = useIntl();
const textLength = length(text);
const progress = textLength / max;
return (
<ProgressCircle
title={intl.formatMessage(messages.title, { chars: textLength, maxChars: max })}
progress={progress}
radius={10}
stroke={3}
/>
);
};
export default VisualCharacterCounter;

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors';
import AutosuggestAccount from '../components/autosuggest_account';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { id }) => ({
account: getAccount(state, id),
});
return mapStateToProps;
};
export default connect(makeMapStateToProps)(AutosuggestAccount);

@ -1,52 +0,0 @@
import { connect } from 'react-redux';
import { openModal } from 'soapbox/actions/modals';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors';
import { getFeatures } from 'soapbox/utils/features';
import ReplyMentions from '../components/reply_mentions';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
return state => {
const instance = state.get('instance');
const { explicitAddressing } = getFeatures(instance);
if (!explicitAddressing) {
return {
explicitAddressing: false,
};
}
const status = getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) });
if (!status) {
return {
isReply: false,
};
}
const to = state.getIn(['compose', 'to']);
const me = state.get('me');
const account = state.getIn(['accounts', me]);
const parentTo = statusToMentionsAccountIdsArray(state, status, account);
return {
to,
parentTo,
isReply: true,
explicitAddressing: true,
};
};
};
const mapDispatchToProps = dispatch => ({
onOpenMentionsModal() {
dispatch(openModal('REPLY_MENTIONS'));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyMentions);

@ -1,9 +0,0 @@
import { connect } from 'react-redux';
import UploadForm from '../components/upload_form';
const mapStateToProps = state => ({
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
});
export default connect(mapStateToProps)(UploadForm);

@ -2,7 +2,7 @@ import { urlRegex } from './url_regex';
const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) {
export function countableText(inputText: string) {
return inputText
.replace(urlRegex, urlPlaceholder)
.replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3');

@ -1,7 +1,6 @@
const regexen = {};
const regexen: { [x: string]: string | RegExp } = {};
const regexSupplant = function(regex, flags) {
flags = flags || '';
const regexSupplant = function(regex: string | RegExp, flags = '') {
if (typeof regex !== 'string') {
if (regex.global && flags.indexOf('g') < 0) {
flags += 'g';
@ -24,7 +23,7 @@ const regexSupplant = function(regex, flags) {
}), flags);
};
const stringSupplant = function(str, values) {
const stringSupplant = function(str: string, values: { [x: string]: any; }) {
return str.replace(/#\{(\w+)\}/g, function(match, name) {
return values[name] || '';
});

@ -1,59 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
const messages = defineMessages({
collapse: { id: 'accordion.collapse', defaultMessage: 'Collapse' },
expand: { id: 'accordion.expand', defaultMessage: 'Expand' },
});
export default @injectIntl class Accordion extends React.PureComponent {
static propTypes = {
headline: PropTypes.node.isRequired,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element, PropTypes.node]),
menu: PropTypes.array,
expanded: PropTypes.bool,
onToggle: PropTypes.func,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
expanded: false,
onToggle: () => {},
}
handleToggle = (e) => {
this.props.onToggle(!this.props.expanded);
e.preventDefault();
}
render() {
const { headline, children, menu, expanded, intl } = this.props;
return (
<div className={classNames('accordion', { 'accordion--expanded': expanded })}>
{menu && (
<div className='accordion__menu'>
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} direction='right' />
</div>
)}
<button
type='button'
className='accordion__title'
onClick={this.handleToggle}
title={intl.formatMessage(expanded ? messages.collapse : messages.expand)}
>
{headline}
</button>
<div className='accordion__content'>
{children}
</div>
</div>
);
}
}

@ -0,0 +1,52 @@
import classNames from 'classnames';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
import type { Menu } from 'soapbox/components/dropdown_menu';
const messages = defineMessages({
collapse: { id: 'accordion.collapse', defaultMessage: 'Collapse' },
expand: { id: 'accordion.expand', defaultMessage: 'Expand' },
});
interface IAccordion {
headline: React.ReactNode,
children?: string | React.ReactNode,
menu?: Menu,
expanded?: boolean,
onToggle?: (value: boolean) => void,
}
const Accordion: React.FC<IAccordion> = ({ headline, children, menu, expanded = false, onToggle = () => {} }) => {
const intl = useIntl();
const handleToggle = (e: React.MouseEvent<HTMLButtonElement>) => {
onToggle(!expanded);
e.preventDefault();
};
return (
<div className={classNames('accordion', { 'accordion--expanded': expanded })}>
{menu && (
<div className='accordion__menu'>
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} />
</div>
)}
<button
type='button'
className='accordion__title'
onClick={handleToggle}
title={intl.formatMessage(expanded ? messages.collapse : messages.expand)}
>
{headline}
</button>
<div className='accordion__content'>
{children}
</div>
</div>
);
};
export default Accordion;

@ -1,130 +0,0 @@
import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
import FilterBar from 'soapbox/components/filter_bar';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
all: { id: 'reactions.all', defaultMessage: 'All' },
});
const mapStateToProps = (state, props) => {
const favourites = state.getIn(['user_lists', 'favourited_by', props.statusId]);
const reactions = state.getIn(['user_lists', 'reactions', props.statusId]);
const allReactions = favourites && reactions && ImmutableList(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).concat(reactions || []);
return {
reactions: allReactions,
};
};
export default @connect(mapStateToProps)
@injectIntl
class ReactionsModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
statusId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
reaction: PropTypes.string,
dispatch: PropTypes.func.isRequired,
reactions: ImmutablePropTypes.list,
};
state = {
reaction: this.props.reaction,
}
fetchData = () => {
const { dispatch, statusId } = this.props;
dispatch(fetchFavourites(statusId));
dispatch(fetchReactions(statusId));
}
componentDidMount() {
this.fetchData();
}
onClickClose = () => {
this.props.onClose('REACTIONS');
};
handleFilterChange = (reaction) => () => {
this.setState({ reaction });
};
renderFilterBar() {
const { intl, reactions } = this.props;
const { reaction } = this.state;
const items = [
{
text: intl.formatMessage(messages.all),
action: this.handleFilterChange(''),
name: 'all',
},
];
reactions.forEach(reaction => items.push(
{
text: `${reaction.name} ${reaction.count}`,
action: this.handleFilterChange(reaction.name),
name: reaction.name,
},
));
return <FilterBar className='reaction__filter-bar' items={items} active={reaction || 'all'} />;
}
render() {
const { reactions } = this.props;
const { reaction } = this.state;
const accounts = reactions && (reaction
? reactions.find(reaction => reaction.name === this.state.reaction)?.accounts.map(account => ({ id: account, reaction: this.state.reaction }))
: reactions.map(reaction => reaction?.accounts.map(account => ({ id: account, reaction: reaction.name }))).flatten());
let body;
if (!accounts) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
body = (<>
{reactions.size > 0 && this.renderFilterBar()}
<ScrollableList
scrollKey='reactions'
emptyMessage={emptyMessage}
className='space-y-3'
>
{accounts.map((account) =>
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} withNote={false} reaction={account.reaction} />,
)}
</ScrollableList>
</>);
}
return (
<Modal
title={<FormattedMessage id='column.reactions' defaultMessage='Reactions' />}
onClose={this.onClickClose}
>
{body}
</Modal>
);
}
}

@ -0,0 +1,110 @@
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
import FilterBar from 'soapbox/components/filter_bar';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
all: { id: 'reactions.all', defaultMessage: 'All' },
});
interface IReactionsModal {
onClose: (string: string) => void,
statusId: string,
username: string,
reaction?: string,
}
const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, ...props }) => {
const dispatch = useDispatch();
const intl = useIntl();
const [reaction, setReaction] = useState(props.reaction);
const reactions = useAppSelector<Array<{
accounts: Array<string>,
count: number,
name: string,
}>>((state) => {
const favourites = state.user_lists.getIn(['favourited_by', statusId]);
const reactions = state.user_lists.getIn(['reactions', statusId]);
return favourites && reactions && ImmutableList(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).concat(reactions || []);
});
const fetchData = () => {
dispatch(fetchFavourites(statusId));
dispatch(fetchReactions(statusId));
};
const onClickClose = () => {
onClose('REACTIONS');
};
const renderFilterBar = () => {
const items = [
{
text: intl.formatMessage(messages.all),
action: () => setReaction(''),
name: 'all',
},
];
reactions.forEach(reaction => items.push(
{
text: `${reaction.name} ${reaction.count}`,
action: () => setReaction(reaction.name),
name: reaction.name,
},
));
return <FilterBar className='reaction__filter-bar' items={items} active={reaction || 'all'} />;
};
useEffect(() => {
fetchData();
}, []);
const accounts = reactions && (reaction
? reactions.find(({ name }) => name === reaction)?.accounts.map(account => ({ id: account, reaction: reaction }))
: reactions.map(({ accounts, name }) => accounts.map(account => ({ id: account, reaction: name }))).flat());
let body;
if (!accounts) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
body = (<>
{reactions.length > 0 && renderFilterBar()}
<ScrollableList
scrollKey='reactions'
emptyMessage={emptyMessage}
className='space-y-3'
>
{accounts.map((account) =>
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} /* reaction={account.reaction} */ />,
)}
</ScrollableList>
</>);
}
return (
<Modal
title={<FormattedMessage id='column.reactions' defaultMessage='Reactions' />}
onClose={onClickClose}
>
{body}
</Modal>
);
};
export default ReactionsModal;

@ -1,85 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import IconButton from 'soapbox/components/icon_button';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors';
import Account from '../../reply_mentions/account';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
return state => {
const status = getStatus(state, { id: state.getIn(['compose', 'in_reply_to']) });
if (!status) {
return {
isReply: false,
};
}
const me = state.get('me');
const account = state.getIn(['accounts', me]);
const mentions = statusToMentionsAccountIdsArray(state, status, account);
return {
mentions,
author: status.getIn(['account', 'id']),
isReply: true,
};
};
};
class ReplyMentionsModal extends ImmutablePureComponent {
static propTypes = {
mentions: ImmutablePropTypes.OrderedSet,
author: PropTypes.string,
intl: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
dispatch: PropTypes.func.isRequired,
};
onClickClose = () => {
const { onClose } = this.props;
onClose('REPLY_MENTIONS');
};
render() {
const { intl, mentions, author } = this.props;
return (
<div className='modal-root__modal reply-mentions-modal'>
<div className='reply-mentions-modal__header'>
<IconButton
className='reply-mentions-modal__back'
src={require('@tabler/icons/icons/arrow-left.svg')}
onClick={this.onClickClose}
aria-label={intl.formatMessage(messages.close)}
title={intl.formatMessage(messages.close)}
/>
<h3 className='reply-mentions-modal__header__title'>
<FormattedMessage id='navigation_bar.in_reply_to' defaultMessage='In reply to' />
</h3>
</div>
<div className='reply-mentions-modal__accounts'>
{mentions.map(accountId => <Account key={accountId} accountId={accountId} added author={author === accountId} />)}
</div>
</div>
);
}
}
export default injectIntl(connect(makeMapStateToProps)(ReplyMentionsModal));

@ -0,0 +1,40 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import { statusToMentionsAccountIdsArray } from 'soapbox/reducers/compose';
import { makeGetStatus } from 'soapbox/selectors';
import Account from '../../reply_mentions/account';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
interface IReplyMentionsModal {
onClose: (string: string) => void,
}
const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
const status = useAppSelector<StatusEntity | null>(state => makeGetStatus()(state, { id: state.compose.get('in_reply_to') }));
const account = useAppSelector((state) => state.accounts.get(state.me));
const mentions = statusToMentionsAccountIdsArray(status, account);
const author = (status?.account as AccountEntity).id;
const onClickClose = () => {
onClose('REPLY_MENTIONS');
};
return (
<Modal
title={<FormattedMessage id='navigation_bar.in_reply_to' defaultMessage='In reply to' />}
onClose={onClickClose}
>
<div className='reply-mentions-modal__accounts'>
{mentions.map(accountId => <Account key={accountId} accountId={accountId} added author={author === accountId} />)}
</div>
</Modal>
);
};
export default ReplyMentionsModal;

@ -113,7 +113,7 @@ export const statusToMentionsArray = (status, account) => {
.delete(account.get('acct'));
};
export const statusToMentionsAccountIdsArray = (state, status, account) => {
export const statusToMentionsAccountIdsArray = (status, account) => {
const author = status.getIn(['account', 'id']);
const mentions = status.get('mentions', []).map(m => m.get('id'));

Loading…
Cancel
Save