Merge remote-tracking branch 'parent/main' into upstream-20240319

This commit is contained in:
KMY 2024-03-19 09:15:20 +09:00
commit 76598bd542
496 changed files with 5795 additions and 3709 deletions

View file

@ -262,18 +262,20 @@ class About extends PureComponent {
<>
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
<div className='about__domain-blocks'>
{domainBlocks.get('items').map(block => (
<div className='about__domain-blocks__domain' key={block.get('domain')}>
<div className='about__domain-blocks__domain__header'>
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity_ex') || block.get('severity')].title)}</span>
</div>
{domainBlocks.get('items').size > 0 && (
<div className='about__domain-blocks'>
{domainBlocks.get('items').map(block => (
<div className='about__domain-blocks__domain' key={block.get('domain')}>
<div className='about__domain-blocks__domain__header'>
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity_ex') || block.get('severity')].title)}</span>
</div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
</div>
))}
</div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
</div>
))}
</div>
)}
</>
) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>

View file

@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import { useState, useRef, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BadgeIcon from '@/material-icons/400-24px/badge.svg?react';
import GlobeIcon from '@/material-icons/400-24px/globe.svg?react';
import { Icon } from 'mastodon/components/icon';
export const DomainPill = ({ domain, username, isSelf }) => {
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const triggerRef = useRef(null);
const handleClick = useCallback(() => {
setOpen(!open);
}, [open, setOpen]);
const handleExpandClick = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
return (
<>
<button className={classNames('account__domain-pill', { active: open })} ref={triggerRef} onClick={handleClick}>{domain}</button>
<Overlay show={open} rootClose onHide={handleClick} offset={[5, 5]} target={triggerRef}>
{({ props }) => (
<div {...props} className='account__domain-pill__popout dropdown-animation'>
<div className='account__domain-pill__popout__header'>
<div className='account__domain-pill__popout__header__icon'><Icon icon={BadgeIcon} /></div>
<h3><FormattedMessage id='domain_pill.whats_in_a_handle' defaultMessage="What's in a handle?" /></h3>
</div>
<div className='account__domain-pill__popout__handle'>
<div className='account__domain-pill__popout__handle__label'>{isSelf ? <FormattedMessage id='domain_pill.your_handle' defaultMessage='Your handle:' /> : <FormattedMessage id='domain_pill.their_handle' defaultMessage='Their handle:' />}</div>
<div className='account__domain-pill__popout__handle__handle'>@{username}@{domain}</div>
</div>
<div className='account__domain-pill__popout__parts'>
<div>
<div className='account__domain-pill__popout__parts__icon'><Icon icon={AlternateEmailIcon} /></div>
<div>
<h6><FormattedMessage id='domain_pill.username' defaultMessage='Username' /></h6>
<p>{isSelf ? <FormattedMessage id='domain_pill.your_username' defaultMessage='Your unique identifier on this server. Its possible to find users with the same username on different servers.' /> : <FormattedMessage id='domain_pill.their_username' defaultMessage='Their unique identifier on their server. Its possible to find users with the same username on different servers.' />}</p>
</div>
</div>
<div>
<div className='account__domain-pill__popout__parts__icon'><Icon icon={GlobeIcon} /></div>
<div>
<h6><FormattedMessage id='domain_pill.server' defaultMessage='Server' /></h6>
<p>{isSelf ? <FormattedMessage id='domain_pill.your_server' defaultMessage='Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.' /> : <FormattedMessage id='domain_pill.their_server' defaultMessage='Their digital home, where all of their posts live.' />}</p>
</div>
</div>
</div>
<p>{isSelf ? <FormattedMessage id='domain_pill.who_you_are' defaultMessage='Because your handle says who you are and where you are, people can interact with you across the social web of <button>ActivityPub-powered platforms</button>.' values={{ button: x => <button onClick={handleExpandClick} className='link-button'>{x}</button> }} /> : <FormattedMessage id='domain_pill.who_they_are' defaultMessage='Since handles say who someone is and where they are, you can interact with people across the social web of <button>ActivityPub-powered platforms</button>.' values={{ button: x => <button onClick={handleExpandClick} className='link-button'>{x}</button> }} />}</p>
{expanded && (
<>
<p><FormattedMessage id='domain_pill.activitypub_like_language' defaultMessage='ActivityPub is like the language Mastodon speaks with other social networks.' /></p>
<p><FormattedMessage id='domain_pill.activitypub_lets_connect' defaultMessage='It lets you connect and interact with people not just on Mastodon, but across different social apps too.' /></p>
</>
)}
</div>
)}
</Overlay>
</>
);
};
DomainPill.propTypes = {
username: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
isSelf: PropTypes.bool,
};

View file

@ -25,13 +25,15 @@ import { IconButton } from 'mastodon/components/icon_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif, me, domain, isShowItem } from 'mastodon/initial_state';
import { autoPlayGif, me, domain as localDomain, isShowItem } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { DomainPill } from './domain_pill';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -81,7 +83,7 @@ const messages = defineMessages({
const titleFromAccount = account => {
const displayName = account.get('display_name');
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct');
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct');
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`;
@ -258,7 +260,7 @@ class Header extends ImmutablePureComponent {
}
render () {
const { account, hidden, intl, domain } = this.props;
const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.context.identity;
if (!account) {
@ -404,7 +406,8 @@ class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1;
const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
const username = account.get('acct').split('@')[0];
const domain = isLocal ? localDomain : account.get('acct').split('@')[1];
const isIndexable = !account.get('noindex');
const badges = [];
@ -449,7 +452,9 @@ class Header extends ImmutablePureComponent {
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} />
<small>
<span>@{acct}</span> {lockedIcon}
<span>@{username}<span className='invisible'>@{domain}</span></span>
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} />
{lockedIcon}
</small>
</h1>
</div>

View file

@ -75,11 +75,7 @@ class Header extends ImmutablePureComponent {
};
handleBlockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
if (!domain) return;
this.props.onBlockDomain(domain);
this.props.onBlockDomain(this.props.account);
};
handleUnblockDomain = () => {

View file

@ -17,7 +17,7 @@ import {
mentionCompose,
directCompose,
} from '../../../actions/compose';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports';
@ -140,15 +140,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onBlockDomain (domain) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)),
},
}));
onBlockDomain (account) {
dispatch(initDomainBlockModal(account));
},
onUnblockDomain (domain) {

View file

@ -7,7 +7,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { Icon } from 'mastodon/components/icon';
import { LoadMore } from 'mastodon/components/load_more';
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
return (
<div className='search-results'>
<div className='search-results__header'>
<Icon id='search' icon={SearchIcon} />
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
</div>
{accounts}
{hashtags}
{statuses}

View file

@ -12,6 +12,7 @@ import { connect } from 'react-redux';
import CirclesIcon from '@/material-icons/400-24px/account_circle-fill.svg?react';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@ -20,7 +21,6 @@ import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
import { fetchFollowRequests } from 'mastodon/actions/accounts';
import Column from 'mastodon/components/column';
@ -117,7 +117,7 @@ class GettingStarted extends ImmutablePureComponent {
if (showTrends) {
navItems.push(
<ColumnLink key='explore' icon='explore' iconComponent={TagIcon} text={intl.formatMessage(messages.explore)} to='/explore' />,
<ColumnLink key='explore' icon='explore' iconComponent={ExploreIcon} text={intl.formatMessage(messages.explore)} to='/explore' />,
);
}

View file

@ -38,7 +38,7 @@ export const FilteredNotificationsBanner = () => {
<div className='filtered-notifications-banner__text'>
<strong><FormattedMessage id='filtered_notifications_banner.title' defaultMessage='Filtered notifications' /></strong>
<span><FormattedMessage id='filtered_notifications_banner.pending_requests' defaultMessage='Notifications from {count, plural, =0 {no} one {one person} other {# people}} you may know' values={{ count: policy.getIn(['summary', 'pending_requests_count']) }} /></span>
<span><FormattedMessage id='filtered_notifications_banner.pending_requests' defaultMessage='Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know' values={{ count: policy.getIn(['summary', 'pending_requests_count']) }} /></span>
</div>
<div className='filtered-notifications-banner__badge'>

View file

@ -15,6 +15,7 @@ import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { IconButton } from 'mastodon/components/icon_button';
import ScrollableList from 'mastodon/components/scrollable_list';
import { SensitiveMediaContextProvider } from 'mastodon/features/ui/util/sensitive_media_context';
import NotificationContainer from './containers/notification_container';
@ -87,7 +88,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
}
}, [dispatch, accountId]);
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') });
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') || account?.get('username') });
return (
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
@ -106,25 +107,27 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
)}
/>
<ScrollableList
scrollKey={`notification_requests/${id}`}
trackScroll={!multiColumn}
bindToDocument={!multiColumn}
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
onLoadMore={handleLoadMore}
>
{notifications.map(item => (
item && <NotificationContainer
key={item.get('id')}
notification={item}
accountId={item.get('account')}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
/>
))}
</ScrollableList>
<SensitiveMediaContextProvider hideMediaByDefault>
<ScrollableList
scrollKey={`notification_requests/${id}`}
trackScroll={!multiColumn}
bindToDocument={!multiColumn}
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
onLoadMore={handleLoadMore}
>
{notifications.map(item => (
item && <NotificationContainer
key={item.get('id')}
notification={item}
accountId={item.get('account')}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
/>
))}
</ScrollableList>
</SensitiveMediaContextProvider>
<Helmet>
<title>{columnTitle}</title>

View file

@ -189,7 +189,7 @@ class ActionBar extends PureComponent {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
onBlockDomain(account);
};
handleUnblockDomain = () => {

View file

@ -9,10 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import ReferenceIcon from '@/material-icons/400-24px/link.svg?react';
import EmojiReactionIcon from '@/material-icons/400-24px/mood.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import { AnimatedNumber } from 'mastodon/components/animated_number';
import EditedTimestamp from 'mastodon/components/edited_timestamp';
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
@ -151,12 +147,9 @@ class DetailedStatus extends ImmutablePureComponent {
let isCardMediaWithSensitive = false;
let applicationLink = '';
let reblogLink = '';
const reblogIcon = 'retweet';
const reblogIconComponent = RepeatIcon;
let favouriteLink = '';
let emojiReactionsLink = '';
let statusReferencesLink = '';
let edited = '';
if (this.props.measureHeight) {
outerStyle.height = `${this.state.height}px`;
@ -238,56 +231,50 @@ class DetailedStatus extends ImmutablePureComponent {
}
if (status.get('application')) {
applicationLink = <> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
}
const visibilityLink = <> · <VisibilityIcon visibility={status.get('limited_scope') || status.get('visibility_ex')} /></>;
const searchabilityLink = <> · <SearchabilityIcon searchability={status.get('searchability')} /></>;
const visibilityLink = <>·<VisibilityIcon visibility={status.get('limited_scope') || status.get('visibility_ex')} /></>;
const searchabilityLink = <>·<SearchabilityIcon searchability={status.get('searchability')} /></>;
if (['private', 'direct'].includes(status.get('visibility_ex'))) {
reblogLink = '';
} else if (this.props.history) {
reblogLink = (
<>
{' · '}
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} icon={reblogIconComponent} />
<span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} />
</span>
</Link>
</>
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} />
</span>
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
</Link>
);
} else {
reblogLink = (
<>
{' · '}
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id={reblogIcon} icon={reblogIconComponent} />
<span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} />
</span>
</a>
</>
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<span className='detailed-status__reblogs'>
<AnimatedNumber value={status.get('reblogs_count')} />
</span>
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
</a>
);
}
if (this.props.history) {
favouriteLink = (
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
<Icon id='star' icon={StarIcon} />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} />
</span>
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
</Link>
);
} else {
favouriteLink = (
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id='star' icon={StarIcon} />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} />
</span>
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
</a>
);
}
@ -295,19 +282,19 @@ class DetailedStatus extends ImmutablePureComponent {
if (this.props.history) {
emojiReactionsLink = (
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/emoji_reactions`} className='detailed-status__link'>
<Icon id='smile-o' icon={EmojiReactionIcon} />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('emoji_reactions_count')} />
</span>
<FormattedMessage id='status.emoji_reactions' defaultMessage='{count, plural, one {reaction} other {reactions}}' values={{ count: status.get('emoji_reactions_count') }} />
</Link>
);
} else {
emojiReactionsLink = (
<a href={`/interact/${status.get('id')}?type=emoji_reactions`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id='smile-o' icon={EmojiReactionIcon} />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('emoji_reactions_count')} />
</span>
<FormattedMessage id='status.emoji_reactions' defaultMessage='{count, plural, one {reaction} other {reactions}}' values={{ count: status.get('emoji_reactions_count') }} />
</a>
);
}
@ -315,32 +302,23 @@ class DetailedStatus extends ImmutablePureComponent {
if (this.props.history) {
statusReferencesLink = (
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/references`} className='detailed-status__link'>
<Icon id='link' icon={ReferenceIcon} />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('status_referred_by_count')} />
</span>
<FormattedMessage id='status.status_references' defaultMessage='{count, plural, one {ref} other {refs}}' values={{ count: status.get('status_referred_by_count') }} />
</Link>
);
} else {
statusReferencesLink = (
<a href={`/interact/${status.get('id')}?type=references`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id='link' icon={ReferenceIcon} />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('status_referred_by_count')} />
</span>
<FormattedMessage id='status.status_references' defaultMessage='{count, plural, one {ref} other {refs}}' values={{ count: status.get('status_referred_by_count') }} />
</a>
);
}
if (status.get('edited_at')) {
edited = (
<>
{' · '}
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
</>
);
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
@ -373,9 +351,28 @@ class DetailedStatus extends ImmutablePureComponent {
{emojiReactionsBar}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{edited}{visibilityLink}{searchabilityLink}{applicationLink}{reblogLink} · {favouriteLink} · {emojiReactionsLink} · {statusReferencesLink}
<div className='detailed-status__meta__line'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>
{visibilityLink}
{searchabilityLink}
{applicationLink}
</div>
{status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
<div className='detailed-status__meta__line'>
{reblogLink}
{reblogLink && <>·</>}
{favouriteLink}
·
{emojiReactionsLink}
·
{statusReferencesLink}
</div>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
@ -35,7 +35,7 @@ import {
insertReferenceCompose,
} from '../../actions/compose';
import {
blockDomain,
initDomainBlockModal,
unblockDomain,
} from '../../actions/domain_blocks';
import {
@ -525,15 +525,8 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(unblockAccount(account.get('id')));
};
handleBlockDomainClick = domain => {
this.props.dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
},
}));
handleBlockDomainClick = account => {
this.props.dispatch(initDomainBlockModal(account));
};
handleUnblockDomainClick = domain => {

View file

@ -1,100 +1,116 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useCallback, useState } from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import classNames from 'classnames';
import { blockAccount } from '../../../actions/accounts';
import { closeModal } from '../../../actions/modal';
import { initReport } from '../../../actions/reports';
import { Button } from '../../../components/button';
import { makeGetAccount } from '../../../selectors';
import { useDispatch } from 'react-redux';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'mastodon/actions/accounts';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
const mapStateToProps = state => ({
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])),
});
export const BlockModal = ({ accountId, acct }) => {
const dispatch = useDispatch();
const [expanded, setExpanded] = useState(false);
return mapStateToProps;
};
const domain = acct.split('@')[1];
const mapDispatchToProps = dispatch => {
return {
onConfirm(account) {
dispatch(blockAccount(account.get('id')));
},
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
onBlockAndReport(account) {
dispatch(blockAccount(account.get('id')));
dispatch(initReport(account));
},
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
onClose() {
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
},
};
};
const handleToggleLearnMore = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
class BlockModal extends PureComponent {
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={BlockIcon} />
</div>
static propTypes = {
account: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
onBlockAndReport: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account);
};
handleSecondary = () => {
this.props.onClose();
this.props.onBlockAndReport(this.props.account);
};
handleCancel = () => {
this.props.onClose();
};
render () {
const { account } = this.props;
return (
<div className='modal-root__modal block-modal'>
<div className='block-modal__container'>
<p>
<FormattedMessage
id='confirmations.block.message'
defaultMessage='Are you sure you want to block {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
</p>
<div>
<h1><FormattedMessage id='block_modal.title' defaultMessage='Block user?' /></h1>
<div>@{acct}</div>
</div>
</div>
<div className='block-modal__action-bar'>
<Button onClick={this.handleCancel} className='block-modal__cancel-button'>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='block_modal.they_will_know' defaultMessage="They can see that they're blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='block_modal.they_cant_see_posts' defaultMessage="They can't see your posts and you won't see theirs." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={AlternateEmailIcon} /></div>
<div><FormattedMessage id='block_modal.you_wont_see_mentions' defaultMessage="You won't see posts that mentions them." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='block_modal.they_cant_mention' defaultMessage="They can't mention or follow you." /></div>
</div>
</div>
</div>
<div className={classNames('safety-action-modal__bottom', { active: expanded })}>
{domain && (
<div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__caveats'>
<FormattedMessage
id='block_modal.remote_users_caveat'
defaultMessage='We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.'
values={{ domain: <strong>{domain}</strong> }}
/>
</div>
</div>
)}
<div className='safety-action-modal__actions'>
{domain && (
<button onClick={handleToggleLearnMore} className='link-button'>
{expanded ? <FormattedMessage id='block_modal.show_less' defaultMessage='Show less' /> : <FormattedMessage id='block_modal.show_more' defaultMessage='Show more' />}
</button>
)}
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' />
</Button>
<Button onClick={this.handleClick} autoFocus>
</button>
<Button onClick={handleClick}>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button>
</div>
</div>
);
}
</div>
);
};
}
BlockModal.propTypes = {
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(BlockModal));
export default BlockModal;

View file

@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import DomainDisabledIcon from '@/material-icons/400-24px/domain_disabled.svg?react';
import HistoryIcon from '@/material-icons/400-24px/history.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import { blockAccount } from 'mastodon/actions/accounts';
import { blockDomain } from 'mastodon/actions/domain_blocks';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
export const DomainBlockModal = ({ domain, accountId, acct }) => {
const dispatch = useDispatch();
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockDomain(domain));
}, [dispatch, domain]);
const handleSecondaryClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(blockAccount(accountId));
}, [dispatch, accountId]);
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={DomainDisabledIcon} />
</div>
<div>
<h1><FormattedMessage id='domain_block_modal.title' defaultMessage='Block domain?' /></h1>
<div>{domain}</div>
</div>
</div>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_wont_know' defaultMessage="They won't know they've been blocked." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_wont_see_posts' defaultMessage="You won't see posts or notifications from users on this server." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={PersonRemoveIcon} /></div>
<div><FormattedMessage id='domain_block_modal.you_will_lose_followers' defaultMessage='All your followers from this server will be removed.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_cant_follow' defaultMessage='Nobody from this server can follow you.' /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={HistoryIcon} /></div>
<div><FormattedMessage id='domain_block_modal.they_can_interact_with_old_posts' defaultMessage='People from this server can interact with your old posts.' /></div>
</div>
</div>
</div>
<div className='safety-action-modal__bottom'>
<div className='safety-action-modal__actions'>
<Button onClick={handleSecondaryClick} secondary>
<FormattedMessage id='domain_block_modal.block_account_instead' defaultMessage='Block @{name} instead' values={{ name: acct.split('@')[0] }} />
</Button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</button>
<Button onClick={handleClick}>
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
</Button>
</div>
</div>
</div>
);
};
DomainBlockModal.propTypes = {
domain: PropTypes.string.isRequired,
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default DomainBlockModal;

View file

@ -7,6 +7,7 @@ import Base from 'mastodon/components/modal_root';
import {
MuteModal,
BlockModal,
DomainBlockModal,
ReportModal,
EmbedModal,
ListEditor,
@ -46,6 +47,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal,

View file

@ -1,138 +1,154 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useCallback, useState } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import classNames from 'classnames';
import Toggle from 'react-toggle';
import { useDispatch } from 'react-redux';
import { muteAccount } from '../../../actions/accounts';
import { closeModal } from '../../../actions/modal';
import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes';
import { Button } from '../../../components/button';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { muteAccount } from 'mastodon/actions/accounts';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { CheckBox } from 'mastodon/components/check_box';
import { Icon } from 'mastodon/components/icon';
import { RadioButton } from 'mastodon/components/radio_button';
const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Indefinite' },
indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Until I unmute them' },
hideFromNotifications: { id: 'mute_modal.hide_from_notifications', defaultMessage: 'Hide from notifications' },
});
const mapStateToProps = state => {
return {
account: state.getIn(['mutes', 'new', 'account']),
notifications: state.getIn(['mutes', 'new', 'notifications']),
muteDuration: state.getIn(['mutes', 'new', 'duration']),
};
const RadioButtonLabel = ({ name, value, currentValue, onChange, label }) => (
<RadioButton
name={name}
value={value}
checked={value === currentValue}
onChange={onChange}
label={label}
/>
);
RadioButtonLabel.propTypes = {
name: PropTypes.string,
value: PropTypes.oneOf([PropTypes.string, PropTypes.number, PropTypes.bool]),
currentValue: PropTypes.oneOf([PropTypes.string, PropTypes.number, PropTypes.bool]),
checked: PropTypes.bool,
onChange: PropTypes.func,
label: PropTypes.node,
};
const mapDispatchToProps = dispatch => {
return {
onConfirm(account, notifications, muteDuration) {
dispatch(muteAccount(account.get('id'), notifications, muteDuration));
},
export const MuteModal = ({ accountId, acct }) => {
const intl = useIntl();
const dispatch = useDispatch();
const [notifications, setNotifications] = useState(true);
const [muteDuration, setMuteDuration] = useState('0');
const [expanded, setExpanded] = useState(false);
onClose() {
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
},
const handleClick = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(muteAccount(accountId, notifications, muteDuration));
}, [dispatch, accountId, notifications, muteDuration]);
onToggleNotifications() {
dispatch(toggleHideNotifications());
},
const handleCancel = useCallback(() => {
dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, [dispatch]);
onChangeMuteDuration(e) {
dispatch(changeMuteDuration(e.target.value));
},
};
};
const handleToggleNotifications = useCallback(({ target }) => {
setNotifications(target.checked);
}, [setNotifications]);
class MuteModal extends PureComponent {
const handleChangeMuteDuration = useCallback(({ target }) => {
setMuteDuration(target.value);
}, [setMuteDuration]);
static propTypes = {
account: PropTypes.object.isRequired,
notifications: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
onToggleNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
muteDuration: PropTypes.number.isRequired,
onChangeMuteDuration: PropTypes.func.isRequired,
};
const handleToggleSettings = useCallback(() => {
setExpanded(!expanded);
}, [expanded, setExpanded]);
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
};
handleCancel = () => {
this.props.onClose();
};
toggleNotifications = () => {
this.props.onToggleNotifications();
};
changeMuteDuration = (e) => {
this.props.onChangeMuteDuration(e);
};
render () {
const { account, notifications, muteDuration, intl } = this.props;
return (
<div className='modal-root__modal mute-modal'>
<div className='mute-modal__container'>
<p>
<FormattedMessage
id='confirmations.mute.message'
defaultMessage='Are you sure you want to mute {name}?'
values={{ name: <strong>@{account.get('acct')}</strong> }}
/>
</p>
<p className='mute-modal__explanation'>
<FormattedMessage
id='confirmations.mute.explanation'
defaultMessage='This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.'
/>
</p>
<div className='setting-toggle'>
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
<label className='setting-toggle__label' htmlFor='mute-modal__hide-notifications-checkbox'>
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
</label>
return (
<div className='modal-root__modal safety-action-modal'>
<div className='safety-action-modal__top'>
<div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
<Icon icon={VolumeOffIcon} />
</div>
<div>
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
<select value={muteDuration} onChange={this.changeMuteDuration}>
<option value={0}>{intl.formatMessage(messages.indefinite)}</option>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
</select>
<div>
<h1><FormattedMessage id='mute_modal.title' defaultMessage='Mute user?' /></h1>
<div>@{acct}</div>
</div>
</div>
<div className='mute-modal__action-bar'>
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
<div className='safety-action-modal__bullet-points'>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CampaignIcon} /></div>
<div><FormattedMessage id='mute_modal.they_wont_know' defaultMessage="They won't know they've been muted." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={VisibilityOffIcon} /></div>
<div><FormattedMessage id='mute_modal.you_wont_see_posts' defaultMessage="They can still see your posts, but you won't see theirs." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={AlternateEmailIcon} /></div>
<div><FormattedMessage id='mute_modal.you_wont_see_mentions' defaultMessage="You won't see posts that mention them." /></div>
</div>
<div>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={ReplyIcon} /></div>
<div><FormattedMessage id='mute_modal.they_can_mention_and_follow' defaultMessage="They can mention and follow you, but you won't see them." /></div>
</div>
</div>
</div>
<div className={classNames('safety-action-modal__bottom', { active: expanded })}>
<div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'>
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
<RadioButtonLabel name='duration' value='x' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
</div>
<div className='safety-action-modal__field-group'>
<CheckBox label={intl.formatMessage(messages.hideFromNotifications)} checked={notifications} onChange={handleToggleNotifications} />
</div>
</div>
<div className='safety-action-modal__actions'>
<button onClick={handleToggleSettings} className='link-button'>
{expanded ? <FormattedMessage id='mute_modal.hide_options' defaultMessage='Hide options' /> : <FormattedMessage id='mute_modal.show_options' defaultMessage='Show options' />}
</button>
<div className='spacer' />
<button onClick={handleCancel} className='link-button'>
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button>
<Button onClick={this.handleClick} autoFocus>
</button>
<Button onClick={handleClick}>
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
</Button>
</div>
</div>
);
}
</div>
);
};
}
MuteModal.propTypes = {
accountId: PropTypes.string.isRequired,
acct: PropTypes.string.isRequired,
};
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(MuteModal));
export default MuteModal;

View file

@ -70,8 +70,8 @@ const NotificationsLink = () => {
<ColumnLink
transparent
to='/notifications'
icon={<IconWithBadge icon={NotificationsIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge icon={NotificationsActiveIcon} count={count} className='column-link__icon' />}
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={count} className='column-link__icon' />}
text={intl.formatMessage(messages.notifications)}
/>
);
@ -94,8 +94,8 @@ const FollowRequestsLink = () => {
<ColumnLink
transparent
to='/follow_requests'
icon={<IconWithBadge icon={PersonAddIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge icon={PersonAddActiveIcon} count={count} className='column-link__icon' />}
icon={<IconWithBadge id='user-plus' icon={PersonAddIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='user-plus' icon={PersonAddActiveIcon} count={count} className='column-link__icon' />}
text={intl.formatMessage(messages.followRequests)}
/>
);

View file

@ -162,6 +162,10 @@ export function BlockModal () {
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal');
}
export function DomainBlockModal () {
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/domain_block_modal');
}
export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
}

View file

@ -0,0 +1,28 @@
import { createContext, useContext, useMemo } from 'react';
export const SensitiveMediaContext = createContext<{
hideMediaByDefault: boolean;
}>({
hideMediaByDefault: false,
});
export function useSensitiveMediaContext() {
return useContext(SensitiveMediaContext);
}
type ContextValue = React.ContextType<typeof SensitiveMediaContext>;
export const SensitiveMediaContextProvider: React.FC<
React.PropsWithChildren<{ hideMediaByDefault: boolean }>
> = ({ hideMediaByDefault, children }) => {
const contextValue = useMemo<ContextValue>(
() => ({ hideMediaByDefault }),
[hideMediaByDefault],
);
return (
<SensitiveMediaContext.Provider value={contextValue}>
{children}
</SensitiveMediaContext.Provider>
);
};