Merge branch 'glitch-soc' into develop

This commit is contained in:
Jeremy Kescher 2024-03-19 18:31:27 +01:00
commit a53f2cf970
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
219 changed files with 3518 additions and 1626 deletions

View file

@ -1,13 +0,0 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2024-01-09 11:30:07 -0500 using Haml-Lint version 0.53.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 1
LineLength:
exclude:
- 'app/views/admin/roles/_form.html.haml'

View file

@ -1,6 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController class ActivityPub::BaseController < Api::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :require_authenticated_user! skip_before_action :require_authenticated_user!
skip_before_action :require_not_suspended! skip_before_action :require_not_suspended!
skip_around_action :set_locale skip_around_action :set_locale

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::ClaimsController < ActivityPub::BaseController class ActivityPub::ClaimsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :require_account_signature! before_action :require_account_signature!

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::CollectionsController < ActivityPub::BaseController class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? } vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseController
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? } vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature! before_action :require_account_signature!

View file

@ -1,9 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::InboxesController < ActivityPub::BaseController class ActivityPub::InboxesController < ActivityPub::BaseController
include SignatureVerification
include JsonLdHelper include JsonLdHelper
include AccountOwnedConcern
before_action :skip_unknown_actor_activity before_action :skip_unknown_actor_activity
before_action :require_actor_signature! before_action :require_actor_signature!

View file

@ -3,9 +3,6 @@
class ActivityPub::OutboxesController < ActivityPub::BaseController class ActivityPub::OutboxesController < ActivityPub::BaseController
LIMIT = 20 LIMIT = 20
include SignatureVerification
include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? } vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?

View file

@ -1,9 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::RepliesController < ActivityPub::BaseController class ActivityPub::RepliesController < ActivityPub::BaseController
include SignatureVerification
include Authorization include Authorization
include AccountOwnedConcern
DESCENDANTS_LIMIT = 60 DESCENDANTS_LIMIT = 60

View file

@ -6,6 +6,8 @@ class InstanceActorsController < ActivityPub::BaseController
serialization_scope nil serialization_scope nil
before_action :set_account before_action :set_account
skip_before_action :authenticate_user! # From `AccountOwnedConcern`
skip_before_action :require_functional! skip_before_action :require_functional!
skip_before_action :update_user_sign_in skip_before_action :update_user_sign_in
@ -16,6 +18,11 @@ class InstanceActorsController < ActivityPub::BaseController
private private
# Skips various `before_action` from `AccountOwnedConcern`
def account_required?
false
end
def set_account def set_account
@account = Account.representative @account = Account.representative
end end

View file

@ -1,25 +0,0 @@
// This file will be loaded on embed pages, regardless of theme.
import 'packs/public-path';
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
function setEmbedHeight () {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
}
if (['interactive', 'complete'].includes(document.readyState)) {
setEmbedHeight();
} else {
document.addEventListener('DOMContentLoaded', setEmbedHeight);
}
});

View file

@ -0,0 +1,41 @@
// This file will be loaded on embed pages, regardless of theme.
import 'packs/public-path';
import ready from '../mastodon/ready';
interface SetHeightMessage {
type: 'setHeight';
id: string;
height: number;
}
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
if (
data &&
typeof data === 'object' &&
'type' in data &&
data.type === 'setHeight'
)
return true;
else return false;
}
window.addEventListener('message', (e) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
const data = e.data;
ready(() => {
window.parent.postMessage(
{
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
},
'*',
);
}).catch((e) => {
console.error('Error in setHeightMessage postMessage', e);
});
});

View file

@ -1,44 +0,0 @@
// This file will be loaded on settings pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
Rails.delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
const avatar = document.getElementById(target.id + '-preview');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
const input = target.parentNode.querySelector('.input-copy__wrapper input');
const oldReadOnly = input.readonly;
input.readonly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
target.parentNode.classList.add('copied');
setTimeout(() => {
target.parentNode.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readonly = oldReadOnly;
});

View file

@ -0,0 +1,70 @@
// This file will be loaded on settings pages, regardless of theme.
import 'packs/public-path';
import Rails from '@rails/ujs';
Rails.delegate(
document,
'#edit_profile input[type=file]',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
const avatar = document.querySelector<HTMLImageElement>(
`img#${target.id}-preview`,
);
if (!avatar) return;
let file: File | undefined;
if (target.files) file = target.files[0];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
if (url) avatar.src = url;
},
);
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
target.focus();
target.select();
target.setSelectionRange(0, target.value.length);
});
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!(target instanceof HTMLButtonElement)) return;
const input = target.parentNode?.querySelector<HTMLInputElement>(
'.input-copy__wrapper input',
);
if (!input) return;
const oldReadOnly = input.readOnly;
input.readOnly = false;
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement;
if (!parent) return;
parent.classList.add('copied');
setTimeout(() => {
parent.classList.remove('copied');
}, 700);
}
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly;
});

View file

@ -7,7 +7,7 @@ pack:
common: common:
filename: common.js filename: common.js
stylesheet: true stylesheet: true
embed: embed.js embed: embed.ts
error: error:
home: home:
inert: inert:
@ -18,7 +18,7 @@ pack:
stylesheet: true stylesheet: true
modal: modal:
public: public:
settings: settings.js settings: settings.ts
sign_up: sign_up:
share: share:
remote_interaction_helper: remote_interaction_helper.ts remote_interaction_helper: remote_interaction_helper.ts

View file

@ -12,8 +12,6 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
export function fetchBlocks() { export function fetchBlocks() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchBlocksRequest()); dispatch(fetchBlocksRequest());
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) { export function initBlockModal(account) {
return dispatch => { return dispatch => {
dispatch({ dispatch(openModal({
type: BLOCKS_INIT_MODAL, modalType: 'BLOCK',
account, modalProps: {
}); accountId: account.get('id'),
acct: account.get('acct'),
dispatch(openModal({ modalType: 'BLOCK' })); },
}));
}; };
} }

View file

@ -1,6 +1,8 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed"; import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
import { openModal } from './modal';
export * from "./domain_blocks_typed"; export * from "./domain_blocks_typed";
@ -150,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error, error,
}; };
} }
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
domain: account.get('acct').split('@')[1],
acct: account.get('acct'),
accountId: account.get('id'),
},
}));

View file

@ -12,10 +12,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() { export function fetchMutes() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchMutesRequest()); dispatch(fetchMutesRequest());
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) { export function initMuteModal(account) {
return dispatch => { return dispatch => {
dispatch({ dispatch(openModal({
type: MUTES_INIT_MODAL, modalType: 'MUTE',
account, modalProps: {
}); accountId: account.get('id'),
acct: account.get('acct'),
dispatch(openModal({ modalType: 'MUTE' })); },
}; }));
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
}; };
} }

View file

@ -0,0 +1,39 @@
import classNames from 'classnames';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from './icon';
interface Props {
value: string;
checked: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
}
export const CheckBox: React.FC<Props> = ({
name,
value,
checked,
onChange,
label,
}) => {
return (
<label className='check-box'>
<input
name={name}
type='checkbox'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('check-box__input', { checked })}>
{checked && <Icon id='check' icon={DoneIcon} />}
</span>
<span>{label}</span>
</label>
);
};

View file

@ -102,7 +102,7 @@ const getUnitDelay = (units: string) => {
}; };
export const timeAgoString = ( export const timeAgoString = (
intl: IntlShape, intl: Pick<IntlShape, 'formatDate' | 'formatMessage'>,
date: Date, date: Date,
now: number, now: number,
year: number, year: number,

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 'flavours/glitch/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

@ -23,7 +23,7 @@ import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; import { autoPlayGif, me, domain as localDomain } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
@ -31,6 +31,8 @@ import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container'; import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { DomainPill } from './domain_pill';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -75,7 +77,7 @@ const messages = defineMessages({
const titleFromAccount = account => { const titleFromAccount = account => {
const displayName = account.get('display_name'); 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; const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`; return `${prefix} (@${acct})`;
@ -167,7 +169,7 @@ class Header extends ImmutablePureComponent {
}; };
render () { render () {
const { account, hidden, intl, domain } = this.props; const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.context.identity; const { signedIn, permissions } = this.context.identity;
if (!account) { if (!account) {
@ -314,7 +316,8 @@ class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') }; const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields'); const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1; 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 isIndexable = !account.get('noindex');
const badges = []; const badges = [];
@ -359,7 +362,9 @@ class Header extends ImmutablePureComponent {
<h1> <h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> <span dangerouslySetInnerHTML={displayNameHtml} />
<small> <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> </small>
</h1> </h1>
</div> </div>

View file

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

View file

@ -15,7 +15,7 @@ import {
mentionCompose, mentionCompose,
directCompose, directCompose,
} from '../../../actions/compose'; } from '../../../actions/compose';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes'; import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports'; import { initReport } from '../../../actions/reports';
@ -138,15 +138,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onBlockDomain (domain) { onBlockDomain (account) {
dispatch(openModal({ dispatch(initDomainBlockModal(account));
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)),
},
}));
}, },
onUnblockDomain (domain) { onUnblockDomain (domain) {

View file

@ -11,6 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.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 PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@ -22,7 +23,6 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { fetchLists } from 'flavours/glitch/actions/lists'; import { fetchLists } from 'flavours/glitch/actions/lists';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
@ -158,7 +158,7 @@ class GettingStarted extends ImmutablePureComponent {
} }
if (showTrends) { if (showTrends) {
navItems.push(<ColumnLink key='explore' icon='explore' iconComponent={TagIcon} text={intl.formatMessage(messages.explore)} to='/explore' />); navItems.push(<ColumnLink key='explore' icon='explore' iconComponent={ExploreIcon} text={intl.formatMessage(messages.explore)} to='/explore' />);
} }
if (signedIn) { if (signedIn) {

View file

@ -1,100 +1,116 @@
import PropTypes from 'prop-types'; 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 { useDispatch } from 'react-redux';
import { closeModal } from '../../../actions/modal';
import { initReport } from '../../../actions/reports';
import { Button } from '../../../components/button';
import { makeGetAccount } from '../../../selectors';
const makeMapStateToProps = () => { import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
const getAccount = makeGetAccount(); 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 'flavours/glitch/actions/accounts';
import { closeModal } from 'flavours/glitch/actions/modal';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
const mapStateToProps = state => ({ export const BlockModal = ({ accountId, acct }) => {
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])), const dispatch = useDispatch();
}); const [expanded, setExpanded] = useState(false);
return mapStateToProps; const domain = acct.split('@')[1];
};
const mapDispatchToProps = dispatch => { const handleClick = useCallback(() => {
return { dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
onConfirm(account) { dispatch(blockAccount(accountId));
dispatch(blockAccount(account.get('id'))); }, [dispatch, accountId]);
},
onBlockAndReport(account) { const handleCancel = useCallback(() => {
dispatch(blockAccount(account.get('id'))); dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(initReport(account)); }, [dispatch]);
},
onClose() { const handleToggleLearnMore = useCallback(() => {
dispatch(closeModal({ setExpanded(!expanded);
modalType: undefined, }, [expanded, setExpanded]);
ignoreFocus: false,
}));
},
};
};
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 = { <div>
account: PropTypes.object.isRequired, <h1><FormattedMessage id='block_modal.title' defaultMessage='Block user?' /></h1>
onClose: PropTypes.func.isRequired, <div>@{acct}</div>
onBlockAndReport: PropTypes.func.isRequired, </div>
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> </div>
<div className='block-modal__action-bar'> <div className='safety-action-modal__bullet-points'>
<Button onClick={this.handleCancel} className='block-modal__cancel-button'> <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' /> <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button> </button>
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' /> <Button onClick={handleClick}>
</Button>
<Button onClick={this.handleClick} autoFocus>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' /> <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button> </Button>
</div> </div>
</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 'flavours/glitch/actions/accounts';
import { blockDomain } from 'flavours/glitch/actions/domain_blocks';
import { closeModal } from 'flavours/glitch/actions/modal';
import { Button } from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/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 'flavours/glitch/components/modal_root';
import { import {
MuteModal, MuteModal,
BlockModal, BlockModal,
DomainBlockModal,
ReportModal, ReportModal,
SettingsModal, SettingsModal,
EmbedModal, EmbedModal,
@ -48,6 +49,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal, 'MUTE': MuteModal,
'BLOCK': BlockModal, 'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
'REPORT': ReportModal, 'REPORT': ReportModal,
'SETTINGS': SettingsModal, 'SETTINGS': SettingsModal,
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }), 'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),

View file

@ -1,138 +1,154 @@
import PropTypes from 'prop-types'; 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 AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes'; import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import { Button } from '../../../components/button'; 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 'flavours/glitch/actions/accounts';
import { closeModal } from 'flavours/glitch/actions/modal';
import { Button } from 'flavours/glitch/components/button';
import { CheckBox } from 'flavours/glitch/components/check_box';
import { Icon } from 'flavours/glitch/components/icon';
import { RadioButton } from 'flavours/glitch/components/radio_button';
const messages = defineMessages({ const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, 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 => { const RadioButtonLabel = ({ name, value, currentValue, onChange, label }) => (
return { <RadioButton
account: state.getIn(['mutes', 'new', 'account']), name={name}
notifications: state.getIn(['mutes', 'new', 'notifications']), value={value}
muteDuration: state.getIn(['mutes', 'new', 'duration']), 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 => { export const MuteModal = ({ accountId, acct }) => {
return { const intl = useIntl();
onConfirm(account, notifications, muteDuration) { const dispatch = useDispatch();
dispatch(muteAccount(account.get('id'), notifications, muteDuration)); const [notifications, setNotifications] = useState(true);
}, const [muteDuration, setMuteDuration] = useState('0');
const [expanded, setExpanded] = useState(false);
onClose() { const handleClick = useCallback(() => {
dispatch(closeModal({ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
modalType: undefined, dispatch(muteAccount(accountId, notifications, muteDuration));
ignoreFocus: false, }, [dispatch, accountId, notifications, muteDuration]);
}));
},
onToggleNotifications() { const handleCancel = useCallback(() => {
dispatch(toggleHideNotifications()); dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, }, [dispatch]);
onChangeMuteDuration(e) { const handleToggleNotifications = useCallback(({ target }) => {
dispatch(changeMuteDuration(e.target.value)); setNotifications(target.checked);
}, }, [setNotifications]);
};
};
class MuteModal extends PureComponent { const handleChangeMuteDuration = useCallback(({ target }) => {
setMuteDuration(target.value);
}, [setMuteDuration]);
static propTypes = { const handleToggleSettings = useCallback(() => {
account: PropTypes.object.isRequired, setExpanded(!expanded);
notifications: PropTypes.bool.isRequired, }, [expanded, setExpanded]);
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
onToggleNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
muteDuration: PropTypes.number.isRequired,
onChangeMuteDuration: PropTypes.func.isRequired,
};
handleClick = () => { return (
this.props.onClose(); <div className='modal-root__modal safety-action-modal'>
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration); <div className='safety-action-modal__top'>
}; <div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
handleCancel = () => { <Icon icon={VolumeOffIcon} />
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>
</div> </div>
<div>
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
<select value={muteDuration} onChange={this.changeMuteDuration}> <div>
<option value={0}>{intl.formatMessage(messages.indefinite)}</option> <h1><FormattedMessage id='mute_modal.title' defaultMessage='Mute user?' /></h1>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <div>@{acct}</div>
<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> </div>
</div> </div>
<div className='mute-modal__action-bar'> <div className='safety-action-modal__bullet-points'>
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'> <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' /> <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button> </button>
<Button onClick={this.handleClick} autoFocus>
<Button onClick={handleClick}>
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' /> <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
</Button> </Button>
</div> </div>
</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

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

View file

@ -1,241 +0,0 @@
import 'packs/public-path';
import { createRoot } from 'react-dom/client';
import { IntlMessageFormat } from 'intl-messageformat';
import { defineMessages } from 'react-intl';
import Rails from '@rails/ujs';
import axios from 'axios';
import { createBrowserHistory } from 'history';
import { throttle } from 'lodash';
import { timeAgoString } from 'flavours/glitch/components/relative_timestamp';
import emojify from 'flavours/glitch/features/emoji/emoji';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import { loadLocale, getLocale } from 'flavours/glitch/locales';
import { loadPolyfills } from 'flavours/glitch/polyfills';
const messages = defineMessages({
usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
passwordExceedsLength: { id: 'password_confirmation.exceeds_maxlength', defaultMessage: 'Password confirmation exceeds the maximum password length' },
passwordDoesNotMatch: { id: 'password_confirmation.mismatching', defaultMessage: 'Password confirmation does not match' },
});
function main() {
const { messages: localeData } = getLocale();
const scrollToDetailedStatus = () => {
const history = createBrowserHistory();
const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
const location = history.location;
if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
detailedStatuses[0].scrollIntoView();
history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
}
};
const getEmojiAnimationHandler = (swapTo) => {
return ({ target }) => {
target.src = target.getAttribute(swapTo);
};
};
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
timeFormat: false,
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
});
const formatMessage = ({ id, defaultMessage }, values) => {
const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
return messageFormat.format(values);
};
document.querySelectorAll('.emojify').forEach((content) => {
content.innerHTML = emojify(content.innerHTML);
});
document.querySelectorAll('time.formatted').forEach((content) => {
const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate;
content.textContent = formattedDate;
});
const isToday = date => {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
document.querySelectorAll('time.relative-formatted').forEach((content) => {
const datetime = new Date(content.getAttribute('datetime'));
let formattedContent;
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
formattedContent = todayFormat.format({ time: formattedTime });
} else {
formattedContent = dateFormat.format(datetime);
}
content.title = formattedContent;
content.textContent = formattedContent;
});
document.querySelectorAll('time.time-ago').forEach((content) => {
const datetime = new Date(content.getAttribute('datetime'));
const now = new Date();
const timeGiven = content.getAttribute('datetime').includes('T');
content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
content.textContent = timeAgoString({
formatMessage,
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, now.getFullYear(), timeGiven);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container')
.then(({ default: MediaContainer }) => {
reactComponents.forEach((component) => {
Array.from(component.children).forEach((child) => {
component.removeChild(child);
});
});
const content = document.createElement('div');
const root = createRoot(content);
root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
.catch(error => {
console.error(error);
scrollToDetailedStatus();
});
} else {
scrollToDetailedStatus();
}
Rails.delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
const username = document.getElementById('user_account_attributes_username');
if (username.value && username.value.length > 0) {
axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
username.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
username.setCustomValidity('');
});
} else {
username.setCustomValidity('');
}
}, 500, { leading: false, trailing: true }));
Rails.delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
const password = document.getElementById('user_password');
const confirmation = document.getElementById('user_password_confirmation');
if (!confirmation) return;
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
} else {
confirmation.setCustomValidity('');
}
});
Rails.delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
Rails.delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
Rails.delegate(document, '.status__content__spoiler-link', 'click', function() {
const statusEl = this.parentNode.parentNode;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
}
return false;
});
document.querySelectorAll('.status__content__spoiler-link').forEach((spoilerLink) => {
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
const toggleSidebar = () => {
const sidebar = document.querySelector('.sidebar ul');
const toggleButton = document.querySelector('.sidebar__toggle__icon');
if (sidebar.classList.contains('visible')) {
document.body.style.overflow = null;
toggleButton.setAttribute('aria-expanded', 'false');
} else {
document.body.style.overflow = 'hidden';
toggleButton.setAttribute('aria-expanded', 'true');
}
toggleButton.classList.toggle('active');
sidebar.classList.toggle('visible');
};
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
toggleSidebar();
});
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleSidebar();
}
});
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
['user_website', 'user_confirm_password', 'registration_user_website', 'registration_user_confirm_password'].forEach(id => {
const field = document.getElementById(id);
if (field) {
field.value = '';
}
});
});
}
loadPolyfills()
.then(loadLocale)
.then(main)
.then(loadKeyboardExtensions)
.catch(error => {
console.error(error);
});

View file

@ -0,0 +1,356 @@
import { createRoot } from 'react-dom/client';
import 'packs/public-path';
import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl';
import Rails from '@rails/ujs';
import axios from 'axios';
import { throttle } from 'lodash';
import { timeAgoString } from 'flavours/glitch/components/relative_timestamp';
import emojify from 'flavours/glitch/features/emoji/emoji';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import { loadLocale, getLocale } from 'flavours/glitch/locales';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
import 'cocoon-js-vanilla';
const messages = defineMessages({
usernameTaken: {
id: 'username.taken',
defaultMessage: 'That username is taken. Try another',
},
passwordExceedsLength: {
id: 'password_confirmation.exceeds_maxlength',
defaultMessage: 'Password confirmation exceeds the maximum password length',
},
passwordDoesNotMatch: {
id: 'password_confirmation.mismatching',
defaultMessage: 'Password confirmation does not match',
},
});
function loaded() {
const { messages: localeData } = getLocale();
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
});
const formatMessage = (
{ id, defaultMessage }: MessageDescriptor,
values?: Record<string, PrimitiveType>,
) => {
let message: string | undefined = undefined;
if (id) message = localeData[id];
if (!message) message = defaultMessage as string;
const messageFormat = new IntlMessageFormat(message, locale);
return messageFormat.format(values) as string;
};
document.querySelectorAll('.emojify').forEach((content) => {
content.innerHTML = emojify(content.innerHTML);
});
document
.querySelectorAll<HTMLTimeElement>('time.formatted')
.forEach((content) => {
const datetime = new Date(content.dateTime);
const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate;
content.textContent = formattedDate;
});
const isToday = (date: Date) => {
const today = new Date();
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
};
const todayFormat = new IntlMessageFormat(
localeData['relative_format.today'] || 'Today at {time}',
locale,
);
document
.querySelectorAll<HTMLTimeElement>('time.relative-formatted')
.forEach((content) => {
const datetime = new Date(content.dateTime);
let formattedContent: string;
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
formattedContent = todayFormat.format({
time: formattedTime,
}) as string;
} else {
formattedContent = dateFormat.format(datetime);
}
content.title = formattedContent;
content.textContent = formattedContent;
});
document
.querySelectorAll<HTMLTimeElement>('time.time-ago')
.forEach((content) => {
const datetime = new Date(content.dateTime);
const now = new Date();
const timeGiven = content.dateTime.includes('T');
content.title = timeGiven
? dateTimeFormat.format(datetime)
: dateFormat.format(datetime);
content.textContent = timeAgoString(
{
formatMessage,
formatDate: (date: Date, options) =>
new Intl.DateTimeFormat(locale, options).format(date),
},
datetime,
now.getTime(),
now.getFullYear(),
timeGiven,
);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(
/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container'
)
.then(({ default: MediaContainer }) => {
reactComponents.forEach((component) => {
Array.from(component.children).forEach((child) => {
component.removeChild(child);
});
});
const content = document.createElement('div');
const root = createRoot(content);
root.render(
<MediaContainer locale={locale} components={reactComponents} />,
);
document.body.appendChild(content);
return true;
})
.catch((error) => {
console.error(error);
});
}
Rails.delegate(
document,
'input#user_account_attributes_username',
'input',
throttle(
({ target }) => {
if (!(target instanceof HTMLInputElement)) return;
if (target.value && target.value.length > 0) {
axios
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
.then(() => {
target.setCustomValidity(formatMessage(messages.usernameTaken));
return true;
})
.catch(() => {
target.setCustomValidity('');
});
} else {
target.setCustomValidity('');
}
},
500,
{ leading: false, trailing: true },
),
);
Rails.delegate(
document,
'#user_password,#user_password_confirmation',
'input',
() => {
const password = document.querySelector<HTMLInputElement>(
'input#user_password',
);
const confirmation = document.querySelector<HTMLInputElement>(
'input#user_password_confirmation',
);
if (!confirmation || !password) return;
if (
confirmation.value &&
confirmation.value.length > password.maxLength
) {
confirmation.setCustomValidity(
formatMessage(messages.passwordExceedsLength),
);
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(
formatMessage(messages.passwordDoesNotMatch),
);
} else {
confirmation.setCustomValidity('');
}
},
);
Rails.delegate(
document,
'button.status__content__spoiler-link',
'click',
function () {
if (!(this instanceof HTMLButtonElement)) return;
const statusEl = this.parentNode?.parentNode;
if (
!(
statusEl instanceof HTMLDivElement &&
statusEl.classList.contains('.status__content')
)
)
return;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = new IntlMessageFormat(
localeData['status.show_more'] || 'Show more',
locale,
).format() as string;
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = new IntlMessageFormat(
localeData['status.show_less'] || 'Show less',
locale,
).format() as string;
}
},
);
document
.querySelectorAll<HTMLButtonElement>('button.status__content__spoiler-link')
.forEach((spoilerLink) => {
const statusEl = spoilerLink.parentNode?.parentNode;
if (
!(
statusEl instanceof HTMLDivElement &&
statusEl.classList.contains('.status__content')
)
)
return;
const message =
statusEl.dataset.spoiler === 'expanded'
? localeData['status.show_less'] || 'Show less'
: localeData['status.show_more'] || 'Show more';
spoilerLink.textContent = new IntlMessageFormat(
message,
locale,
).format() as string;
});
}
const toggleSidebar = () => {
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
const toggleButton = document.querySelector<HTMLAnchorElement>(
'a.sidebar__toggle__icon',
);
if (!sidebar || !toggleButton) return;
if (sidebar.classList.contains('visible')) {
document.body.style.overflow = '';
toggleButton.setAttribute('aria-expanded', 'false');
} else {
document.body.style.overflow = 'hidden';
toggleButton.setAttribute('aria-expanded', 'true');
}
toggleButton.classList.toggle('active');
sidebar.classList.toggle('visible');
};
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
toggleSidebar();
});
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleSidebar();
}
});
Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => {
if (target instanceof HTMLImageElement && target.dataset.original)
target.src = target.dataset.original;
});
Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
if (target instanceof HTMLImageElement && target.dataset.static)
target.src = target.dataset.static;
});
// Empty the honeypot fields in JS in case something like an extension
// automatically filled them.
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
[
'user_website',
'user_confirm_password',
'registration_user_website',
'registration_user_confirm_password',
].forEach((id) => {
const field = document.querySelector<HTMLInputElement>(`input#${id}`);
if (field) {
field.value = '';
}
});
});
function main() {
ready(loaded).catch((error) => {
console.error(error);
});
}
loadPolyfills()
.then(loadLocale)
.then(main)
.then(loadKeyboardExtensions)
.catch((error) => {
console.error(error);
});

View file

@ -1,42 +0,0 @@
import 'packs/public-path';
import Rails from '@rails/ujs';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import 'cocoon-js-vanilla';
function main() {
const toggleSidebar = () => {
const sidebar = document.querySelector('.sidebar ul');
const toggleButton = document.querySelector('.sidebar__toggle__icon');
if (sidebar.classList.contains('visible')) {
document.body.style.overflow = null;
toggleButton.setAttribute('aria-expanded', 'false');
} else {
document.body.style.overflow = 'hidden';
toggleButton.setAttribute('aria-expanded', 'true');
}
toggleButton.classList.toggle('active');
sidebar.classList.toggle('visible');
};
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
toggleSidebar();
});
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', e => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
toggleSidebar();
}
});
}
loadPolyfills()
.then(main)
.then(loadKeyboardExtensions)
.catch(error => {
console.error(error);
});

View file

@ -1,22 +0,0 @@
import Immutable from 'immutable';
import {
BLOCKS_INIT_MODAL,
} from '../actions/blocks';
const initialState = Immutable.Map({
new: Immutable.Map({
account_id: null,
}),
});
export default function mutes(state = initialState, action) {
switch (action.type) {
case BLOCKS_INIT_MODAL:
return state.withMutations((state) => {
state.setIn(['new', 'account_id'], action.account.get('id'));
});
default:
return state;
}
}

View file

@ -7,7 +7,6 @@ import { accountsReducer } from './accounts';
import accounts_map from './accounts_map'; import accounts_map from './accounts_map';
import alerts from './alerts'; import alerts from './alerts';
import announcements from './announcements'; import announcements from './announcements';
import blocks from './blocks';
import boosts from './boosts'; import boosts from './boosts';
import compose from './compose'; import compose from './compose';
import contexts from './contexts'; import contexts from './contexts';
@ -27,7 +26,6 @@ import markers from './markers';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import meta from './meta'; import meta from './meta';
import { modalReducer } from './modal'; import { modalReducer } from './modal';
import mutes from './mutes';
import { notificationPolicyReducer } from './notification_policy'; import { notificationPolicyReducer } from './notification_policy';
import { notificationRequestsReducer } from './notification_requests'; import { notificationRequestsReducer } from './notification_requests';
import notifications from './notifications'; import notifications from './notifications';
@ -65,8 +63,6 @@ const reducers = {
settings, settings,
local_settings, local_settings,
push_notifications, push_notifications,
mutes,
blocks,
boosts, boosts,
server, server,
contexts, contexts,

View file

@ -1,31 +0,0 @@
import Immutable from 'immutable';
import {
MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
MUTES_CHANGE_DURATION,
} from '../actions/mutes';
const initialState = Immutable.Map({
new: Immutable.Map({
account: null,
notifications: true,
duration: 0,
}),
});
export default function mutes(state = initialState, action) {
switch (action.type) {
case MUTES_INIT_MODAL:
return state.withMutations((state) => {
state.setIn(['new', 'account'], action.account);
state.setIn(['new', 'notifications'], true);
});
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
return state.updateIn(['new', 'notifications'], (old) => !old);
case MUTES_CHANGE_DURATION:
return state.setIn(['new', 'duration'], Number(action.duration));
default:
return state;
}
}

View file

@ -106,17 +106,17 @@
} }
&.button-secondary { &.button-secondary {
color: $ui-button-secondary-color; color: $highlight-text-color;
background: transparent; background: transparent;
padding: 6px 17px; padding: 6px 17px;
border: 1px solid $ui-button-secondary-border-color; border: 1px solid $highlight-text-color;
&:active, &:active,
&:focus, &:focus,
&:hover { &:hover {
border-color: $ui-button-secondary-focus-background-color; border-color: lighten($highlight-text-color, 4%);
color: $ui-button-secondary-focus-color; color: lighten($highlight-text-color, 4%);
background-color: $ui-button-secondary-focus-background-color; background-color: transparent;
text-decoration: none; text-decoration: none;
} }
@ -2017,6 +2017,118 @@ body > [data-popper-placement] {
} }
} }
&__domain-pill {
display: inline-flex;
background: rgba($highlight-text-color, 0.2);
border-radius: 4px;
border: 0;
color: $highlight-text-color;
font-weight: 500;
font-size: 12px;
line-height: 16px;
padding: 4px 8px;
&.active {
color: $white;
background: $ui-highlight-color;
}
&__popout {
background: var(--dropdown-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--dropdown-border-color);
box-shadow: var(--dropdown-shadow);
max-width: 320px;
padding: 16px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 24px;
font-size: 14px;
line-height: 20px;
color: $darker-text-color;
.link-button {
display: inline;
font-size: inherit;
line-height: inherit;
}
&__header {
display: flex;
align-items: center;
gap: 12px;
&__icon {
width: 40px;
height: 40px;
background: $ui-highlight-color;
color: $white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
h3 {
font-size: 17px;
line-height: 22px;
color: $primary-text-color;
}
}
&__handle {
border: 2px dashed $highlight-text-color;
background: rgba($highlight-text-color, 0.1);
padding: 12px 8px;
color: $highlight-text-color;
border-radius: 4px;
&__label {
font-size: 11px;
line-height: 16px;
font-weight: 500;
}
&__handle {
user-select: all;
}
}
&__parts {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 12px;
line-height: 16px;
& > div {
display: flex;
align-items: flex-start;
gap: 12px;
}
&__icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: $highlight-text-color;
}
h6 {
font-size: 14px;
line-height: 20px;
font-weight: 500;
color: $primary-text-color;
}
}
}
}
&__note { &__note {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
@ -5883,6 +5995,10 @@ a.status-card {
pointer-events: auto; pointer-events: auto;
user-select: text; user-select: text;
display: flex; display: flex;
@media screen and (max-width: $no-gap-breakpoint) {
margin-top: auto;
}
} }
.video-modal .video-player { .video-modal .video-player {
@ -6214,6 +6330,154 @@ a.status-card {
margin-inline-start: 10px; margin-inline-start: 10px;
} }
.safety-action-modal {
width: 600px;
flex-direction: column;
&__top,
&__bottom {
display: flex;
gap: 8px;
padding: 24px;
flex-direction: column;
background: var(--modal-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--modal-border-color);
}
&__top {
border-radius: 16px 16px 0 0;
border-bottom: 0;
gap: 16px;
}
&__bottom {
border-radius: 0 0 16px 16px;
border-top: 0;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 0;
border-bottom: 0;
padding-bottom: 32px;
}
}
&__header {
display: flex;
gap: 16px;
align-items: center;
font-size: 14px;
line-height: 20px;
color: $darker-text-color;
&__icon {
border-radius: 64px;
background: $ui-highlight-color;
color: $white;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
.icon {
width: 24px;
height: 24px;
}
}
h1 {
font-size: 22px;
line-height: 28px;
color: $primary-text-color;
}
}
&__bullet-points {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 16px;
line-height: 24px;
& > div {
display: flex;
gap: 16px;
align-items: center;
}
&__icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.icon {
width: 24px;
height: 24px;
}
}
}
&__field-group {
display: flex;
flex-direction: column;
label {
display: flex;
gap: 16px;
align-items: center;
font-size: 16px;
line-height: 24px;
height: 32px;
padding: 0 12px;
}
}
&__caveats {
font-size: 14px;
padding: 0 12px;
strong {
font-weight: 500;
}
}
&__bottom {
padding-top: 0;
&__collapsible {
display: none;
flex-direction: column;
gap: 16px;
}
&.active {
background: var(--modal-background-variant-color);
padding-top: 24px;
.safety-action-modal__bottom__collapsible {
display: flex;
}
}
}
&__actions {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
.link-button {
padding: 10px 12px;
font-weight: 600;
}
}
}
.doodle-modal, .doodle-modal,
.boost-modal, .boost-modal,
.confirmation-modal, .confirmation-modal,
@ -7599,7 +7863,8 @@ img.modal-warning {
display: flex; display: flex;
} }
.radio-button { .radio-button,
.check-box {
font-size: 14px; font-size: 14px;
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@ -7618,17 +7883,19 @@ img.modal-warning {
} }
&__input { &__input {
display: block; display: flex;
align-items: center;
justify-content: center;
position: relative; position: relative;
border: 2px solid $secondary-text-color; border: 2px solid $secondary-text-color;
box-sizing: border-box; box-sizing: border-box;
width: 18px; width: 20px;
height: 18px; height: 20px;
flex: 0 0 auto; flex: 0 0 auto;
border-radius: 50%; border-radius: 50%;
&.checked { &.checked {
border-color: $secondary-text-color; border-color: $ui-highlight-color;
&::before { &::before {
position: absolute; position: absolute;
@ -7637,9 +7904,31 @@ img.modal-warning {
content: ''; content: '';
display: block; display: block;
border-radius: 50%; border-radius: 50%;
width: 10px; width: 12px;
height: 10px; height: 12px;
background: $secondary-text-color; background: $ui-highlight-color;
}
}
.icon {
width: 18px;
height: 18px;
}
}
}
.check-box {
&__input {
width: 18px;
height: 18px;
border-radius: 2px;
&.checked {
background: $ui-highlight-color;
color: $white;
&::before {
display: none;
} }
} }
} }
@ -8143,14 +8432,17 @@ noscript {
font-size: 17px; font-size: 17px;
line-height: 22px; line-height: 22px;
color: $primary-text-color; color: $primary-text-color;
font-weight: 700; font-weight: 600;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
small { small {
display: block; display: flex;
font-size: 15px; align-items: center;
gap: 4px;
font-size: 14px;
line-height: 20px;
color: $darker-text-color; color: $darker-text-color;
font-weight: 400; font-weight: 400;
overflow: hidden; overflow: hidden;
@ -8161,10 +8453,8 @@ noscript {
} }
.icon-lock { .icon-lock {
height: 16px; height: 18px;
width: 16px; width: 18px;
position: relative;
top: 3px;
} }
} }
} }
@ -9126,22 +9416,36 @@ noscript {
} }
} }
.safety-action-modal,
.interaction-modal { .interaction-modal {
max-width: 90vw; max-width: 90vw;
width: 600px; width: 600px;
background: var(--modal-background-color); }
border: 1px solid var(--modal-border-color);
border-radius: 8px; .interaction-modal {
overflow: visible; overflow: visible;
position: relative; position: relative;
display: block; display: block;
padding: 40px; border-radius: 16px;
background: var(--modal-background-color);
backdrop-filter: var(--background-filter);
border: 1px solid var(--modal-border-color);
padding: 24px;
@media screen and (max-width: $no-gap-breakpoint) {
border-radius: 16px 16px 0 0;
border-bottom: 0;
padding-bottom: 32px;
}
h3 { h3 {
font-size: 22px; font-size: 22px;
line-height: 33px; line-height: 33px;
font-weight: 700; font-weight: 700;
text-align: center; display: flex;
align-items: center;
justify-content: center;
gap: 8px;
} }
p { p {
@ -9162,7 +9466,9 @@ noscript {
&__icon { &__icon {
color: $highlight-text-color; color: $highlight-text-color;
margin: 0 5px; display: flex;
align-items: center;
justify-content: center;
} }
&__lead { &__lead {
@ -9195,6 +9501,7 @@ noscript {
border: 0; border: 0;
padding: 15px - 4px 15px - 6px; padding: 15px - 4px 15px - 6px;
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0;
&::placeholder { &::placeholder {
color: lighten($darker-text-color, 4%); color: lighten($darker-text-color, 4%);

View file

@ -46,10 +46,10 @@ $ui-button-focus-background-color: $blurple-600 !default;
$ui-button-focus-outline-color: $blurple-400 !default; $ui-button-focus-outline-color: $blurple-400 !default;
$ui-button-focus-outline: solid 2px $ui-button-focus-outline-color !default; $ui-button-focus-outline: solid 2px $ui-button-focus-outline-color !default;
$ui-button-secondary-color: $grey-100 !default; $ui-button-secondary-color: $blurple-500 !default;
$ui-button-secondary-border-color: $grey-100 !default; $ui-button-secondary-border-color: $blurple-500 !default;
$ui-button-secondary-focus-background-color: $grey-600 !default; $ui-button-secondary-focus-border-color: $blurple-300 !default;
$ui-button-secondary-focus-color: $white !default; $ui-button-secondary-focus-color: $blurple-300 !default;
$ui-button-tertiary-color: $blurple-300 !default; $ui-button-tertiary-color: $blurple-300 !default;
$ui-button-tertiary-border-color: $blurple-300 !default; $ui-button-tertiary-border-color: $blurple-300 !default;
@ -104,7 +104,8 @@ $dismiss-overlay-width: 4rem;
--dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)}; --dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)}, --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)}; 0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
--modal-background-color: #{darken($ui-base-color, 4%)}; --modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
--modal-background-variant-color: #{rgba($ui-base-color, 0.7)};
--modal-border-color: #{lighten($ui-base-color, 4%)}; --modal-border-color: #{lighten($ui-base-color, 4%)};
--background-border-color: #{lighten($ui-base-color, 4%)}; --background-border-color: #{lighten($ui-base-color, 4%)};
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); --background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);

View file

@ -2,12 +2,12 @@
pack: pack:
admin: admin:
- packs/admin.tsx - packs/admin.tsx
- packs/public.jsx - packs/public.tsx
auth: packs/public.jsx auth: packs/public.tsx
common: common:
filename: packs/common.js filename: packs/common.js
stylesheet: true stylesheet: true
embed: packs/public.jsx embed: packs/public.tsx
error: packs/error.js error: packs/error.js
home: home:
filename: packs/home.js filename: packs/home.js
@ -17,8 +17,8 @@ pack:
- flavours/glitch/async/notifications - flavours/glitch/async/notifications
mailer: mailer:
modal: modal:
public: packs/public.jsx public: packs/public.tsx
settings: packs/settings.js settings: packs/public.tsx
sign_up: packs/sign_up.js sign_up: packs/sign_up.js
share: packs/share.jsx share: packs/share.jsx

View file

@ -2,12 +2,12 @@
pack: pack:
admin: admin:
- admin.tsx - admin.tsx
- public.jsx - public.tsx
auth: public.jsx auth: public.tsx
common: common:
filename: common.js filename: common.js
stylesheet: true stylesheet: true
embed: public.jsx embed: public.tsx
error: error.js error: error.js
home: home:
filename: application.js filename: application.js
@ -17,8 +17,8 @@ pack:
- features/notifications - features/notifications
mailer: mailer:
modal: modal:
public: public.jsx public: public.tsx
settings: public.jsx settings: public.tsx
sign_up: sign_up.js sign_up: sign_up.js
share: share.jsx share: share.jsx

View file

@ -12,8 +12,6 @@ export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST';
export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS';
export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL';
export const BLOCKS_INIT_MODAL = 'BLOCKS_INIT_MODAL';
export function fetchBlocks() { export function fetchBlocks() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchBlocksRequest()); dispatch(fetchBlocksRequest());
@ -90,11 +88,12 @@ export function expandBlocksFail(error) {
export function initBlockModal(account) { export function initBlockModal(account) {
return dispatch => { return dispatch => {
dispatch({ dispatch(openModal({
type: BLOCKS_INIT_MODAL, modalType: 'BLOCK',
account, modalProps: {
}); accountId: account.get('id'),
acct: account.get('acct'),
dispatch(openModal({ modalType: 'BLOCK' })); },
}));
}; };
} }

View file

@ -1,6 +1,8 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed"; import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed";
import { openModal } from './modal';
export * from "./domain_blocks_typed"; export * from "./domain_blocks_typed";
@ -150,3 +152,12 @@ export function expandDomainBlocksFail(error) {
error, error,
}; };
} }
export const initDomainBlockModal = account => dispatch => dispatch(openModal({
modalType: 'DOMAIN_BLOCK',
modalProps: {
domain: account.get('acct').split('@')[1],
acct: account.get('acct'),
accountId: account.get('id'),
},
}));

View file

@ -12,10 +12,6 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
export function fetchMutes() { export function fetchMutes() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchMutesRequest()); dispatch(fetchMutesRequest());
@ -92,26 +88,12 @@ export function expandMutesFail(error) {
export function initMuteModal(account) { export function initMuteModal(account) {
return dispatch => { return dispatch => {
dispatch({ dispatch(openModal({
type: MUTES_INIT_MODAL, modalType: 'MUTE',
account, modalProps: {
}); accountId: account.get('id'),
acct: account.get('acct'),
dispatch(openModal({ modalType: 'MUTE' })); },
}; }));
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}
export function changeMuteDuration(duration) {
return dispatch => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
}; };
} }

View file

@ -0,0 +1,39 @@
import classNames from 'classnames';
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from './icon';
interface Props {
value: string;
checked: boolean;
name: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
label: React.ReactNode;
}
export const CheckBox: React.FC<Props> = ({
name,
value,
checked,
onChange,
label,
}) => {
return (
<label className='check-box'>
<input
name={name}
type='checkbox'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('check-box__input', { checked })}>
{checked && <Icon id='check' icon={DoneIcon} />}
</span>
<span>{label}</span>
</label>
);
};

View file

@ -102,7 +102,7 @@ const getUnitDelay = (units: string) => {
}; };
export const timeAgoString = ( export const timeAgoString = (
intl: IntlShape, intl: Pick<IntlShape, 'formatDate' | 'formatMessage'>,
date: Date, date: Date,
now: number, now: number,
year: number, year: number,

View file

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

View file

@ -1,4 +1,4 @@
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -15,7 +15,7 @@ import {
directCompose, directCompose,
} from '../actions/compose'; } from '../actions/compose';
import { import {
blockDomain, initDomainBlockModal,
unblockDomain, unblockDomain,
} from '../actions/domain_blocks'; } from '../actions/domain_blocks';
import { import {
@ -253,15 +253,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed)); dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
}, },
onBlockDomain (domain) { onBlockDomain (account) {
dispatch(openModal({ dispatch(initDomainBlockModal(account));
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)),
},
}));
}, },
onUnblockDomain (domain) { onUnblockDomain (domain) {

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 { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'mastodon/initial_state'; import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import AccountNoteContainer from '../containers/account_note_container'; import AccountNoteContainer from '../containers/account_note_container';
import FollowRequestNoteContainer from '../containers/follow_request_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { DomainPill } from './domain_pill';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -78,7 +80,7 @@ const messages = defineMessages({
const titleFromAccount = account => { const titleFromAccount = account => {
const displayName = account.get('display_name'); 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; const prefix = displayName.trim().length === 0 ? account.get('username') : displayName;
return `${prefix} (@${acct})`; return `${prefix} (@${acct})`;
@ -252,7 +254,7 @@ class Header extends ImmutablePureComponent {
} }
render () { render () {
const { account, hidden, intl, domain } = this.props; const { account, hidden, intl } = this.props;
const { signedIn, permissions } = this.context.identity; const { signedIn, permissions } = this.context.identity;
if (!account) { if (!account) {
@ -393,7 +395,8 @@ class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') }; const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields'); const fields = account.get('fields');
const isLocal = account.get('acct').indexOf('@') === -1; 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 isIndexable = !account.get('noindex');
const badges = []; const badges = [];
@ -438,7 +441,9 @@ class Header extends ImmutablePureComponent {
<h1> <h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> <span dangerouslySetInnerHTML={displayNameHtml} />
<small> <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> </small>
</h1> </h1>
</div> </div>

View file

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

View file

@ -17,7 +17,7 @@ import {
mentionCompose, mentionCompose,
directCompose, directCompose,
} from '../../../actions/compose'; } from '../../../actions/compose';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { initDomainBlockModal, unblockDomain } from '../../../actions/domain_blocks';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { initMuteModal } from '../../../actions/mutes'; import { initMuteModal } from '../../../actions/mutes';
import { initReport } from '../../../actions/reports'; import { initReport } from '../../../actions/reports';
@ -140,15 +140,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onBlockDomain (domain) { onBlockDomain (account) {
dispatch(openModal({ dispatch(initDomainBlockModal(account));
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)),
},
}));
}, },
onUnblockDomain (domain) { onUnblockDomain (domain) {

View file

@ -11,6 +11,7 @@ import { connect } from 'react-redux';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.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 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 PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
@ -19,7 +20,6 @@ import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react'; import StarIcon from '@/material-icons/400-24px/star.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { fetchFollowRequests } from 'mastodon/actions/accounts';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
@ -112,7 +112,7 @@ class GettingStarted extends ImmutablePureComponent {
if (showTrends) { if (showTrends) {
navItems.push( 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

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

View file

@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
@ -34,7 +34,7 @@ import {
directCompose, directCompose,
} from '../../actions/compose'; } from '../../actions/compose';
import { import {
blockDomain, initDomainBlockModal,
unblockDomain, unblockDomain,
} from '../../actions/domain_blocks'; } from '../../actions/domain_blocks';
import { import {
@ -463,15 +463,8 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(unblockAccount(account.get('id'))); this.props.dispatch(unblockAccount(account.get('id')));
}; };
handleBlockDomainClick = domain => { handleBlockDomainClick = account => {
this.props.dispatch(openModal({ this.props.dispatch(initDomainBlockModal(account));
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)),
},
}));
}; };
handleUnblockDomainClick = domain => { handleUnblockDomainClick = domain => {

View file

@ -1,100 +1,116 @@
import PropTypes from 'prop-types'; 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 { useDispatch } from 'react-redux';
import { closeModal } from '../../../actions/modal';
import { initReport } from '../../../actions/reports';
import { Button } from '../../../components/button';
import { makeGetAccount } from '../../../selectors';
const makeMapStateToProps = () => { import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
const getAccount = makeGetAccount(); 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 => ({ export const BlockModal = ({ accountId, acct }) => {
account: getAccount(state, state.getIn(['blocks', 'new', 'account_id'])), const dispatch = useDispatch();
}); const [expanded, setExpanded] = useState(false);
return mapStateToProps; const domain = acct.split('@')[1];
};
const mapDispatchToProps = dispatch => { const handleClick = useCallback(() => {
return { dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
onConfirm(account) { dispatch(blockAccount(accountId));
dispatch(blockAccount(account.get('id'))); }, [dispatch, accountId]);
},
onBlockAndReport(account) { const handleCancel = useCallback(() => {
dispatch(blockAccount(account.get('id'))); dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
dispatch(initReport(account)); }, [dispatch]);
},
onClose() { const handleToggleLearnMore = useCallback(() => {
dispatch(closeModal({ setExpanded(!expanded);
modalType: undefined, }, [expanded, setExpanded]);
ignoreFocus: false,
}));
},
};
};
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 = { <div>
account: PropTypes.object.isRequired, <h1><FormattedMessage id='block_modal.title' defaultMessage='Block user?' /></h1>
onClose: PropTypes.func.isRequired, <div>@{acct}</div>
onBlockAndReport: PropTypes.func.isRequired, </div>
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> </div>
<div className='block-modal__action-bar'> <div className='safety-action-modal__bullet-points'>
<Button onClick={this.handleCancel} className='block-modal__cancel-button'> <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' /> <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button> </button>
<Button onClick={this.handleSecondary} className='confirmation-modal__secondary-button'>
<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block & Report' /> <Button onClick={handleClick}>
</Button>
<Button onClick={this.handleClick} autoFocus>
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' /> <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
</Button> </Button>
</div> </div>
</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 { import {
MuteModal, MuteModal,
BlockModal, BlockModal,
DomainBlockModal,
ReportModal, ReportModal,
EmbedModal, EmbedModal,
ListEditor, ListEditor,
@ -41,6 +42,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }), 'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
'MUTE': MuteModal, 'MUTE': MuteModal,
'BLOCK': BlockModal, 'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,
'REPORT': ReportModal, 'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal, 'EMBED': EmbedModal,

View file

@ -1,138 +1,154 @@
import PropTypes from 'prop-types'; 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 AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes'; import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import { Button } from '../../../components/button'; 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({ const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, 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 => { const RadioButtonLabel = ({ name, value, currentValue, onChange, label }) => (
return { <RadioButton
account: state.getIn(['mutes', 'new', 'account']), name={name}
notifications: state.getIn(['mutes', 'new', 'notifications']), value={value}
muteDuration: state.getIn(['mutes', 'new', 'duration']), 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 => { export const MuteModal = ({ accountId, acct }) => {
return { const intl = useIntl();
onConfirm(account, notifications, muteDuration) { const dispatch = useDispatch();
dispatch(muteAccount(account.get('id'), notifications, muteDuration)); const [notifications, setNotifications] = useState(true);
}, const [muteDuration, setMuteDuration] = useState('0');
const [expanded, setExpanded] = useState(false);
onClose() { const handleClick = useCallback(() => {
dispatch(closeModal({ dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
modalType: undefined, dispatch(muteAccount(accountId, notifications, muteDuration));
ignoreFocus: false, }, [dispatch, accountId, notifications, muteDuration]);
}));
},
onToggleNotifications() { const handleCancel = useCallback(() => {
dispatch(toggleHideNotifications()); dispatch(closeModal({ modalType: undefined, ignoreFocus: false }));
}, }, [dispatch]);
onChangeMuteDuration(e) { const handleToggleNotifications = useCallback(({ target }) => {
dispatch(changeMuteDuration(e.target.value)); setNotifications(target.checked);
}, }, [setNotifications]);
};
};
class MuteModal extends PureComponent { const handleChangeMuteDuration = useCallback(({ target }) => {
setMuteDuration(target.value);
}, [setMuteDuration]);
static propTypes = { const handleToggleSettings = useCallback(() => {
account: PropTypes.object.isRequired, setExpanded(!expanded);
notifications: PropTypes.bool.isRequired, }, [expanded, setExpanded]);
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
onToggleNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
muteDuration: PropTypes.number.isRequired,
onChangeMuteDuration: PropTypes.func.isRequired,
};
handleClick = () => { return (
this.props.onClose(); <div className='modal-root__modal safety-action-modal'>
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration); <div className='safety-action-modal__top'>
}; <div className='safety-action-modal__header'>
<div className='safety-action-modal__header__icon'>
handleCancel = () => { <Icon icon={VolumeOffIcon} />
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>
</div> </div>
<div>
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
<select value={muteDuration} onChange={this.changeMuteDuration}> <div>
<option value={0}>{intl.formatMessage(messages.indefinite)}</option> <h1><FormattedMessage id='mute_modal.title' defaultMessage='Mute user?' /></h1>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <div>@{acct}</div>
<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> </div>
</div> </div>
<div className='mute-modal__action-bar'> <div className='safety-action-modal__bullet-points'>
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'> <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' /> <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
</Button> </button>
<Button onClick={this.handleClick} autoFocus>
<Button onClick={handleClick}>
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' /> <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
</Button> </Button>
</div> </div>
</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

@ -118,6 +118,10 @@ export function BlockModal () {
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal'); 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 () { export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
} }

View file

@ -500,7 +500,6 @@
"status.delete": "Borrar", "status.delete": "Borrar",
"status.detailed_status": "Vista de conversación detallada", "status.detailed_status": "Vista de conversación detallada",
"status.edit": "Editar", "status.edit": "Editar",
"status.edited": "Editau {date}",
"status.edited_x_times": "Editau {count, plural, one {{count} vez} other {{count} veces}}", "status.edited_x_times": "Editau {count, plural, one {{count} vez} other {{count} veces}}",
"status.embed": "Incrustado", "status.embed": "Incrustado",
"status.filter": "Filtrar esta publicación", "status.filter": "Filtrar esta publicación",

View file

@ -646,7 +646,6 @@
"status.direct": "إشارة خاصة لـ @{name}", "status.direct": "إشارة خاصة لـ @{name}",
"status.direct_indicator": "إشارة خاصة", "status.direct_indicator": "إشارة خاصة",
"status.edit": "تعديل", "status.edit": "تعديل",
"status.edited": "عُدّل في {date}",
"status.edited_x_times": "عُدّل {count, plural, zero {} one {مرةً واحدة} two {مرّتان} few {{count} مرات} many {{count} مرة} other {{count} مرة}}", "status.edited_x_times": "عُدّل {count, plural, zero {} one {مرةً واحدة} two {مرّتان} few {{count} مرات} many {{count} مرة} other {{count} مرة}}",
"status.embed": "إدماج", "status.embed": "إدماج",
"status.favourite": "فضّل", "status.favourite": "فضّل",

View file

@ -429,7 +429,6 @@
"status.delete": "Desaniciar", "status.delete": "Desaniciar",
"status.direct": "Mentar a @{name} per privao", "status.direct": "Mentar a @{name} per privao",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edited": "Editóse'l {date}",
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}", "status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
"status.embed": "Empotrar", "status.embed": "Empotrar",
"status.filter": "Peñerar esti artículu", "status.filter": "Peñerar esti artículu",

View file

@ -662,10 +662,11 @@
"status.direct": "Згадаць асабіста @{name}", "status.direct": "Згадаць асабіста @{name}",
"status.direct_indicator": "Асабістае згадванне", "status.direct_indicator": "Асабістае згадванне",
"status.edit": "Рэдагаваць", "status.edit": "Рэдагаваць",
"status.edited": "Адрэдагавана {date}", "status.edited": "Апошняе рэдагаванне {date}",
"status.edited_x_times": "Рэдагавана {count, plural, one {{count} раз} few {{count} разы} many {{count} разоў} other {{count} разу}}", "status.edited_x_times": "Рэдагавана {count, plural, one {{count} раз} few {{count} разы} many {{count} разоў} other {{count} разу}}",
"status.embed": "Убудаваць", "status.embed": "Убудаваць",
"status.favourite": "Упадабанае", "status.favourite": "Упадабанае",
"status.favourites": "{count, plural, one {# упадабанае} few {# упадабаныя} many {# упадабаных} other {# упадабанага}}",
"status.filter": "Фільтраваць гэты допіс", "status.filter": "Фільтраваць гэты допіс",
"status.filtered": "Адфільтравана", "status.filtered": "Адфільтравана",
"status.hide": "Схаваць допіс", "status.hide": "Схаваць допіс",
@ -686,6 +687,7 @@
"status.reblog": "Пашырыць", "status.reblog": "Пашырыць",
"status.reblog_private": "Пашырыць з першапачатковай бачнасцю", "status.reblog_private": "Пашырыць з першапачатковай бачнасцю",
"status.reblogged_by": "{name} пашырыў(-ла)", "status.reblogged_by": "{name} пашырыў(-ла)",
"status.reblogs": "{count, plural, one {# пашырэнне} few {# пашырэнні} many {# пашырэнняў} other {# пашырэння}}",
"status.reblogs.empty": "Гэты допіс яшчэ ніхто не пашырыў. Калі гэта адбудзецца, гэтых людзей будзе бачна тут.", "status.reblogs.empty": "Гэты допіс яшчэ ніхто не пашырыў. Калі гэта адбудзецца, гэтых людзей будзе бачна тут.",
"status.redraft": "Выдаліць і паправіць", "status.redraft": "Выдаліць і паправіць",
"status.remove_bookmark": "Выдаліць закладку", "status.remove_bookmark": "Выдаліць закладку",

View file

@ -648,7 +648,6 @@
"status.direct": "Частно споменаване на @{name}", "status.direct": "Частно споменаване на @{name}",
"status.direct_indicator": "Частно споменаване", "status.direct_indicator": "Частно споменаване",
"status.edit": "Редактиране", "status.edit": "Редактиране",
"status.edited": "Редактирано на {date}",
"status.edited_x_times": "Редактирано {count, plural,one {{count} път} other {{count} пъти}}", "status.edited_x_times": "Редактирано {count, plural,one {{count} път} other {{count} пъти}}",
"status.embed": "Вграждане", "status.embed": "Вграждане",
"status.favourite": "Любимо", "status.favourite": "Любимо",

View file

@ -584,7 +584,7 @@
"status.direct": "Menegiñ @{name} ent-prevez", "status.direct": "Menegiñ @{name} ent-prevez",
"status.direct_indicator": "Meneg prevez", "status.direct_indicator": "Meneg prevez",
"status.edit": "Kemmañ", "status.edit": "Kemmañ",
"status.edited": "Aozet {date}", "status.edited": "Kemmet da ziwezhañ d'an {date}",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.embed": "Enframmañ", "status.embed": "Enframmañ",
"status.favourite": "Muiañ-karet", "status.favourite": "Muiañ-karet",

View file

@ -662,7 +662,6 @@
"status.direct": "Menciona privadament @{name}", "status.direct": "Menciona privadament @{name}",
"status.direct_indicator": "Menció privada", "status.direct_indicator": "Menció privada",
"status.edit": "Edita", "status.edit": "Edita",
"status.edited": "Editat {date}",
"status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}", "status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}",
"status.embed": "Incrusta", "status.embed": "Incrusta",
"status.favourite": "Favorit", "status.favourite": "Favorit",

View file

@ -559,7 +559,6 @@
"status.direct": "بە شێوەیەکی تایبەت باسی @{name} بکە", "status.direct": "بە شێوەیەکی تایبەت باسی @{name} بکە",
"status.direct_indicator": "ئاماژەی تایبەت", "status.direct_indicator": "ئاماژەی تایبەت",
"status.edit": "دەستکاری", "status.edit": "دەستکاری",
"status.edited": "بەشداری {date}",
"status.edited_x_times": "دەستکاریکراوە {count, plural, one {{count} کات} other {{count} کات}}", "status.edited_x_times": "دەستکاریکراوە {count, plural, one {{count} کات} other {{count} کات}}",
"status.embed": "نیشتەجێ بکە", "status.embed": "نیشتەجێ بکە",
"status.filter": "ئەم پۆستە فلتەر بکە", "status.filter": "ئەم پۆستە فلتەر بکە",

View file

@ -646,7 +646,6 @@
"status.direct": "Soukromě zmínit @{name}", "status.direct": "Soukromě zmínit @{name}",
"status.direct_indicator": "Soukromá zmínka", "status.direct_indicator": "Soukromá zmínka",
"status.edit": "Upravit", "status.edit": "Upravit",
"status.edited": "Upraveno {date}",
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}", "status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
"status.embed": "Vložit na web", "status.embed": "Vložit na web",
"status.favourite": "Oblíbit", "status.favourite": "Oblíbit",

View file

@ -661,7 +661,6 @@
"status.direct": "Crybwyll yn breifat @{name}", "status.direct": "Crybwyll yn breifat @{name}",
"status.direct_indicator": "Crybwyll preifat", "status.direct_indicator": "Crybwyll preifat",
"status.edit": "Golygu", "status.edit": "Golygu",
"status.edited": "Golygwyd {date}",
"status.edited_x_times": "Golygwyd {count, plural, one {count} two {count} other {{count} gwaith}}", "status.edited_x_times": "Golygwyd {count, plural, one {count} two {count} other {{count} gwaith}}",
"status.embed": "Mewnblannu", "status.embed": "Mewnblannu",
"status.favourite": "Hoffi", "status.favourite": "Hoffi",

View file

@ -662,10 +662,11 @@
"status.direct": "Privat omtale @{name}", "status.direct": "Privat omtale @{name}",
"status.direct_indicator": "Privat omtale", "status.direct_indicator": "Privat omtale",
"status.edit": "Redigér", "status.edit": "Redigér",
"status.edited": "Redigeret {date}", "status.edited": "Senest redigeret {date}",
"status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}", "status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}",
"status.embed": "Indlejr", "status.embed": "Indlejr",
"status.favourite": "Favorit", "status.favourite": "Favorit",
"status.favourites": "{count, plural, one {# favorit} other {# favoritter}}",
"status.filter": "Filtrér dette indlæg", "status.filter": "Filtrér dette indlæg",
"status.filtered": "Filtreret", "status.filtered": "Filtreret",
"status.hide": "Skjul indlæg", "status.hide": "Skjul indlæg",
@ -686,6 +687,7 @@
"status.reblog": "Fremhæv", "status.reblog": "Fremhæv",
"status.reblog_private": "Boost med oprindelig synlighed", "status.reblog_private": "Boost med oprindelig synlighed",
"status.reblogged_by": "{name} fremhævede", "status.reblogged_by": "{name} fremhævede",
"status.reblogs": "{count, plural, one {# boost} other {# boosts}}",
"status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.", "status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.",
"status.redraft": "Slet og omformulér", "status.redraft": "Slet og omformulér",
"status.remove_bookmark": "Fjern bogmærke", "status.remove_bookmark": "Fjern bogmærke",

View file

@ -662,10 +662,11 @@
"status.direct": "@{name} privat erwähnen", "status.direct": "@{name} privat erwähnen",
"status.direct_indicator": "Private Erwähnung", "status.direct_indicator": "Private Erwähnung",
"status.edit": "Beitrag bearbeiten", "status.edit": "Beitrag bearbeiten",
"status.edited": "Bearbeitet {date}", "status.edited": "Zuletzt am {date} bearbeitet",
"status.edited_x_times": "{count, plural, one {{count}-mal} other {{count}-mal}} bearbeitet", "status.edited_x_times": "{count, plural, one {{count}-mal} other {{count}-mal}} bearbeitet",
"status.embed": "Beitrag per iFrame einbetten", "status.embed": "Beitrag per iFrame einbetten",
"status.favourite": "Favorisieren", "status.favourite": "Favorisieren",
"status.favourites": "{count, plural, one {Favorit} other {Favoriten}}",
"status.filter": "Beitrag filtern", "status.filter": "Beitrag filtern",
"status.filtered": "Gefiltert", "status.filtered": "Gefiltert",
"status.hide": "Beitrag ausblenden", "status.hide": "Beitrag ausblenden",
@ -686,6 +687,7 @@
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen", "status.reblog_private": "Mit der ursprünglichen Zielgruppe teilen",
"status.reblogged_by": "{name} teilte", "status.reblogged_by": "{name} teilte",
"status.reblogs": "{count, plural, one {geteilt} other {geteilt}}",
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.", "status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
"status.redraft": "Löschen und neu erstellen", "status.redraft": "Löschen und neu erstellen",
"status.remove_bookmark": "Lesezeichen entfernen", "status.remove_bookmark": "Lesezeichen entfernen",

View file

@ -586,7 +586,6 @@
"status.direct": "Ιδιωτική επισήμανση @{name}", "status.direct": "Ιδιωτική επισήμανση @{name}",
"status.direct_indicator": "Ιδιωτική επισήμανση", "status.direct_indicator": "Ιδιωτική επισήμανση",
"status.edit": "Επεξεργασία", "status.edit": "Επεξεργασία",
"status.edited": "Επεξεργάστηκε στις {date}",
"status.edited_x_times": "Επεξεργάστηκε {count, plural, one {{count} φορά} other {{count} φορές}}", "status.edited_x_times": "Επεξεργάστηκε {count, plural, one {{count} φορά} other {{count} φορές}}",
"status.embed": "Ενσωμάτωσε", "status.embed": "Ενσωμάτωσε",
"status.favourite": "Αγαπημένα", "status.favourite": "Αγαπημένα",

View file

@ -640,7 +640,6 @@
"status.direct": "Privately mention @{name}", "status.direct": "Privately mention @{name}",
"status.direct_indicator": "Private mention", "status.direct_indicator": "Private mention",
"status.edit": "Edit", "status.edit": "Edit",
"status.edited": "Edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}", "status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Embed", "status.embed": "Embed",
"status.favourite": "Favourite", "status.favourite": "Favourite",

View file

@ -89,6 +89,14 @@
"announcement.announcement": "Announcement", "announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)", "attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio", "audio.hide": "Hide audio",
"block_modal.remote_users_caveat": "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.",
"block_modal.show_less": "Show less",
"block_modal.show_more": "Show more",
"block_modal.they_cant_mention": "They can't mention or follow you.",
"block_modal.they_cant_see_posts": "They can't see your posts and you won't see theirs.",
"block_modal.they_will_know": "They can see that they're blocked.",
"block_modal.title": "Block user?",
"block_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"boost_modal.combo": "You can press {combo} to skip this next time", "boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.copy_stacktrace": "Copy error report", "bundle_column_error.copy_stacktrace": "Copy error report",
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.", "bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
@ -160,9 +168,7 @@
"compose_form.spoiler.unmarked": "Add content warning", "compose_form.spoiler.unmarked": "Add content warning",
"compose_form.spoiler_placeholder": "Content warning (optional)", "compose_form.spoiler_placeholder": "Content warning (optional)",
"confirmation_modal.cancel": "Cancel", "confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Block",
"confirmations.block.message": "Are you sure you want to block {name}?",
"confirmations.cancel_follow_request.confirm": "Withdraw request", "confirmations.cancel_follow_request.confirm": "Withdraw request",
"confirmations.cancel_follow_request.message": "Are you sure you want to withdraw your request to follow {name}?", "confirmations.cancel_follow_request.message": "Are you sure you want to withdraw your request to follow {name}?",
"confirmations.delete.confirm": "Delete", "confirmations.delete.confirm": "Delete",
@ -171,15 +177,13 @@
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.discard_edit_media.confirm": "Discard", "confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?", "confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Block entire domain", "confirmations.domain_block.confirm": "Block server",
"confirmations.domain_block.message": "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.", "confirmations.domain_block.message": "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.",
"confirmations.edit.confirm": "Edit", "confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.logout.confirm": "Log out", "confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?", "confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",
"confirmations.mute.explanation": "This will hide posts from them and posts mentioning them, but it will still allow them to see your posts and follow you.",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.", "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.",
"confirmations.reply.confirm": "Reply", "confirmations.reply.confirm": "Reply",
@ -205,6 +209,27 @@
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.", "dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.",
"dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.", "dismissable_banner.explore_tags": "These are hashtags that are gaining traction on the social web today. Hashtags that are used by more different people are ranked higher.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.", "dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"domain_block_modal.block": "Block server",
"domain_block_modal.block_account_instead": "Block @{name} instead",
"domain_block_modal.they_can_interact_with_old_posts": "People from this server can interact with your old posts.",
"domain_block_modal.they_cant_follow": "Nobody from this server can follow you.",
"domain_block_modal.they_wont_know": "They won't know they've been blocked.",
"domain_block_modal.title": "Block domain?",
"domain_block_modal.you_will_lose_followers": "All your followers from this server will be removed.",
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Their handle:",
"domain_pill.their_server": "Their digital home, where all of their posts live.",
"domain_pill.their_username": "Their unique identifier on their server. Its possible to find users with the same username on different servers.",
"domain_pill.username": "Username",
"domain_pill.whats_in_a_handle": "What's in a handle?",
"domain_pill.who_they_are": "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>.",
"domain_pill.who_you_are": "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>.",
"domain_pill.your_handle": "Your handle:",
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.",
"embed.instructions": "Embed this post on your website by copying the code below.", "embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
@ -402,9 +427,15 @@
"loading_indicator.label": "Loading…", "loading_indicator.label": "Loading…",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}", "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.duration": "Duration", "mute_modal.hide_from_notifications": "Hide from notifications",
"mute_modal.hide_notifications": "Hide notifications from this user?", "mute_modal.hide_options": "Hide options",
"mute_modal.indefinite": "Indefinite", "mute_modal.indefinite": "Until I unmute them",
"mute_modal.show_options": "Show options",
"mute_modal.they_can_mention_and_follow": "They can mention and follow you, but you won't see them.",
"mute_modal.they_wont_know": "They won't know they've been muted.",
"mute_modal.title": "Mute user?",
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
"navigation_bar.about": "About", "navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface", "navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users", "navigation_bar.blocks": "Blocked users",

View file

@ -617,7 +617,6 @@
"status.direct": "Private mencii @{name}", "status.direct": "Private mencii @{name}",
"status.direct_indicator": "Privata mencio", "status.direct_indicator": "Privata mencio",
"status.edit": "Redakti", "status.edit": "Redakti",
"status.edited": "Redaktita {date}",
"status.edited_x_times": "Redactita {count, plural, one {{count} fojon} other {{count} fojojn}}", "status.edited_x_times": "Redactita {count, plural, one {{count} fojon} other {{count} fojojn}}",
"status.embed": "Enkorpigi", "status.embed": "Enkorpigi",
"status.favourite": "Ŝatata", "status.favourite": "Ŝatata",

View file

@ -662,10 +662,11 @@
"status.direct": "Mención privada a @{name}", "status.direct": "Mención privada a @{name}",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edit": "Editar", "status.edit": "Editar",
"status.edited": "Editado {date}", "status.edited": "Última edición: {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}", "status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}",
"status.embed": "Insertar", "status.embed": "Insertar",
"status.favourite": "Marcar como favorito", "status.favourite": "Marcar como favorito",
"status.favourites": "{count, plural, one {# voto} other {# votos}}",
"status.filter": "Filtrar este mensaje", "status.filter": "Filtrar este mensaje",
"status.filtered": "Filtrado", "status.filtered": "Filtrado",
"status.hide": "Ocultar mensaje", "status.hide": "Ocultar mensaje",
@ -686,6 +687,7 @@
"status.reblog": "Adherir", "status.reblog": "Adherir",
"status.reblog_private": "Adherir a la audiencia original", "status.reblog_private": "Adherir a la audiencia original",
"status.reblogged_by": "{name} adhirió", "status.reblogged_by": "{name} adhirió",
"status.reblogs": "{count, plural, one {adhesión} other {adhesiones}}",
"status.reblogs.empty": "Todavía nadie adhirió a este mensaje. Cuando alguien lo haga, se mostrará acá.", "status.reblogs.empty": "Todavía nadie adhirió a este mensaje. Cuando alguien lo haga, se mostrará acá.",
"status.redraft": "Eliminar mensaje original y editarlo", "status.redraft": "Eliminar mensaje original y editarlo",
"status.remove_bookmark": "Quitar marcador", "status.remove_bookmark": "Quitar marcador",

View file

@ -662,10 +662,11 @@
"status.direct": "Mención privada @{name}", "status.direct": "Mención privada @{name}",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edit": "Editar", "status.edit": "Editar",
"status.edited": "Editado {date}", "status.edited": "Última edición {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} time} other {{count} veces}}", "status.edited_x_times": "Editado {count, plural, one {{count} time} other {{count} veces}}",
"status.embed": "Incrustado", "status.embed": "Incrustado",
"status.favourite": "Favorito", "status.favourite": "Favorito",
"status.favourites": "{count, plural, one {favorito} other {favoritos}}",
"status.filter": "Filtrar esta publicación", "status.filter": "Filtrar esta publicación",
"status.filtered": "Filtrado", "status.filtered": "Filtrado",
"status.hide": "Ocultar toot", "status.hide": "Ocultar toot",
@ -686,6 +687,7 @@
"status.reblog": "Retootear", "status.reblog": "Retootear",
"status.reblog_private": "Implusar a la audiencia original", "status.reblog_private": "Implusar a la audiencia original",
"status.reblogged_by": "Retooteado por {name}", "status.reblogged_by": "Retooteado por {name}",
"status.reblogs": "{count, plural, one {impulso} other {impulsos}}",
"status.reblogs.empty": "Nadie retooteó este toot todavía. Cuando alguien lo haga, aparecerá aquí.", "status.reblogs.empty": "Nadie retooteó este toot todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.redraft": "Borrar y volver a borrador", "status.redraft": "Borrar y volver a borrador",
"status.remove_bookmark": "Eliminar marcador", "status.remove_bookmark": "Eliminar marcador",

View file

@ -238,11 +238,11 @@
"empty_column.followed_tags": "No has seguido ninguna etiqueta todavía. Cuando lo hagas, se mostrarán aquí.", "empty_column.followed_tags": "No has seguido ninguna etiqueta todavía. Cuando lo hagas, se mostrarán aquí.",
"empty_column.hashtag": "No hay nada en este hashtag aún.", "empty_column.hashtag": "No hay nada en este hashtag aún.",
"empty_column.home": "¡Tu línea temporal está vacía! Sigue a más personas para rellenarla.", "empty_column.home": "¡Tu línea temporal está vacía! Sigue a más personas para rellenarla.",
"empty_column.list": "No hay nada en esta lista aún. Cuando miembros de esta lista publiquen nuevos estatus, estos aparecerán qui.", "empty_column.list": "Aún no hay nada en esta lista. Cuando los miembros de esta lista publiquen nuevos estados, estos aparecerán aquí.",
"empty_column.lists": "No tienes ninguna lista. cuando crees una, se mostrará aquí.", "empty_column.lists": "No tienes ninguna lista. Cuando crees una, se mostrará aquí.",
"empty_column.mutes": "Aún no has silenciado a ningún usuario.", "empty_column.mutes": "Aún no has silenciado a ningún usuario.",
"empty_column.notification_requests": "¡Todo limpio! No hay nada aquí. Cuando recibas nuevas notificaciones, aparecerán aquí conforme a tu configuración.", "empty_column.notification_requests": "¡Todo limpio! No hay nada aquí. Cuando recibas nuevas notificaciones, aparecerán aquí conforme a tu configuración.",
"empty_column.notifications": "No tienes ninguna notificación aún. Interactúa con otros para empezar una conversación.", "empty_column.notifications": "Aún no tienes ninguna notificación. Cuando otras personas interactúen contigo, aparecerán aquí.",
"empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo", "empty_column.public": "¡No hay nada aquí! Escribe algo públicamente, o sigue usuarios de otras instancias manualmente para llenarlo",
"error.unexpected_crash.explanation": "Debido a un error en nuestro código o a un problema de compatibilidad con el navegador, esta página no se ha podido mostrar correctamente.", "error.unexpected_crash.explanation": "Debido a un error en nuestro código o a un problema de compatibilidad con el navegador, esta página no se ha podido mostrar correctamente.",
"error.unexpected_crash.explanation_addons": "No se pudo mostrar correctamente esta página. Este error probablemente fue causado por un complemento del navegador web o por herramientas de traducción automática.", "error.unexpected_crash.explanation_addons": "No se pudo mostrar correctamente esta página. Este error probablemente fue causado por un complemento del navegador web o por herramientas de traducción automática.",
@ -258,7 +258,7 @@
"explore.trending_tags": "Etiquetas", "explore.trending_tags": "Etiquetas",
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que ha accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.", "filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que ha accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.",
"filter_modal.added.context_mismatch_title": "¡El contexto no coincide!", "filter_modal.added.context_mismatch_title": "¡El contexto no coincide!",
"filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado, necesitará cambiar la fecha de caducidad para que se aplique.", "filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado, tendrás que cambiar la fecha de caducidad para que se aplique.",
"filter_modal.added.expired_title": "¡Filtro caducado!", "filter_modal.added.expired_title": "¡Filtro caducado!",
"filter_modal.added.review_and_configure": "Para revisar y configurar esta categoría de filtros, vaya a {settings_link}.", "filter_modal.added.review_and_configure": "Para revisar y configurar esta categoría de filtros, vaya a {settings_link}.",
"filter_modal.added.review_and_configure_title": "Ajustes de filtro", "filter_modal.added.review_and_configure_title": "Ajustes de filtro",
@ -662,10 +662,11 @@
"status.direct": "Mención privada @{name}", "status.direct": "Mención privada @{name}",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edit": "Editar", "status.edit": "Editar",
"status.edited": "Editado {date}", "status.edited": "Última edición {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}", "status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}",
"status.embed": "Incrustado", "status.embed": "Incrustado",
"status.favourite": "Favorito", "status.favourite": "Favorito",
"status.favourites": "{count, plural, one {favorito} other {favoritos}}",
"status.filter": "Filtrar esta publicación", "status.filter": "Filtrar esta publicación",
"status.filtered": "Filtrado", "status.filtered": "Filtrado",
"status.hide": "Ocultar publicación", "status.hide": "Ocultar publicación",
@ -686,6 +687,7 @@
"status.reblog": "Impulsar", "status.reblog": "Impulsar",
"status.reblog_private": "Impulsar a la audiencia original", "status.reblog_private": "Impulsar a la audiencia original",
"status.reblogged_by": "Impulsado por {name}", "status.reblogged_by": "Impulsado por {name}",
"status.reblogs": "{count, plural, one {impulso} other {impulsos}}",
"status.reblogs.empty": "Nadie ha impulsado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.", "status.reblogs.empty": "Nadie ha impulsado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.redraft": "Borrar y volver a borrador", "status.redraft": "Borrar y volver a borrador",
"status.remove_bookmark": "Eliminar marcador", "status.remove_bookmark": "Eliminar marcador",

View file

@ -647,7 +647,6 @@
"status.direct": "Maini privaatselt @{name}", "status.direct": "Maini privaatselt @{name}",
"status.direct_indicator": "Privaatne mainimine", "status.direct_indicator": "Privaatne mainimine",
"status.edit": "Muuda", "status.edit": "Muuda",
"status.edited": "{date} muudetud",
"status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}", "status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}",
"status.embed": "Manustamine", "status.embed": "Manustamine",
"status.favourite": "Lemmik", "status.favourite": "Lemmik",

View file

@ -662,10 +662,11 @@
"status.direct": "Aipatu pribatuki @{name}", "status.direct": "Aipatu pribatuki @{name}",
"status.direct_indicator": "Aipamen pribatua", "status.direct_indicator": "Aipamen pribatua",
"status.edit": "Editatu", "status.edit": "Editatu",
"status.edited": "Editatua {date}", "status.edited": "Azken edizioa: {date}",
"status.edited_x_times": "{count, plural, one {behin} other {{count} aldiz}} editatua", "status.edited_x_times": "{count, plural, one {behin} other {{count} aldiz}} editatua",
"status.embed": "Txertatu", "status.embed": "Txertatu",
"status.favourite": "Gogokoa", "status.favourite": "Gogokoa",
"status.favourites": "{count, plural, one {gogoko} other {gogoko}}",
"status.filter": "Iragazi bidalketa hau", "status.filter": "Iragazi bidalketa hau",
"status.filtered": "Iragazita", "status.filtered": "Iragazita",
"status.hide": "Tuta ezkutatu", "status.hide": "Tuta ezkutatu",
@ -686,6 +687,7 @@
"status.reblog": "Bultzada", "status.reblog": "Bultzada",
"status.reblog_private": "Bultzada jatorrizko hartzaileei", "status.reblog_private": "Bultzada jatorrizko hartzaileei",
"status.reblogged_by": "{name}(r)en bultzada", "status.reblogged_by": "{name}(r)en bultzada",
"status.reblogs": "{count, plural, one {bultzada} other {bultzada}}",
"status.reblogs.empty": "Inork ez dio bultzada eman bidalketa honi oraindik. Inork egiten badu, hemen agertuko da.", "status.reblogs.empty": "Inork ez dio bultzada eman bidalketa honi oraindik. Inork egiten badu, hemen agertuko da.",
"status.redraft": "Ezabatu eta berridatzi", "status.redraft": "Ezabatu eta berridatzi",
"status.remove_bookmark": "Kendu laster-marka", "status.remove_bookmark": "Kendu laster-marka",

View file

@ -646,7 +646,6 @@
"status.direct": "اشارهٔ خصوصی به @{name}", "status.direct": "اشارهٔ خصوصی به @{name}",
"status.direct_indicator": "اشارهٔ خصوصی", "status.direct_indicator": "اشارهٔ خصوصی",
"status.edit": "ویرایش", "status.edit": "ویرایش",
"status.edited": "ویرایش شده در {date}",
"status.edited_x_times": "{count, plural, one {{count} مرتبه} other {{count} مرتبه}} ویرایش شد", "status.edited_x_times": "{count, plural, one {{count} مرتبه} other {{count} مرتبه}} ویرایش شد",
"status.embed": "جاسازی", "status.embed": "جاسازی",
"status.favourite": "برگزیده‌", "status.favourite": "برگزیده‌",

View file

@ -483,7 +483,7 @@
"notifications.policy.filter_not_followers_title": "Henkilöt, jotka eivät seuraa sinua", "notifications.policy.filter_not_followers_title": "Henkilöt, jotka eivät seuraa sinua",
"notifications.policy.filter_not_following_hint": "Kunnes hyväksyt ne manuaalisesti", "notifications.policy.filter_not_following_hint": "Kunnes hyväksyt ne manuaalisesti",
"notifications.policy.filter_not_following_title": "Henkilöt, joita et seuraa", "notifications.policy.filter_not_following_title": "Henkilöt, joita et seuraa",
"notifications.policy.filter_private_mentions_hint": "Suodatetaan, ellei se vastaa omaan mainintaan tai jos seuraat lähettäjää", "notifications.policy.filter_private_mentions_hint": "Suodatetaan, ellei se vastaa omaan mainintaasi tai ellet seuraa lähettäjää",
"notifications.policy.filter_private_mentions_title": "Ei-toivotut yksityismaininnat", "notifications.policy.filter_private_mentions_title": "Ei-toivotut yksityismaininnat",
"notifications.policy.title": "Suodata ilmoitukset pois kohteesta…", "notifications.policy.title": "Suodata ilmoitukset pois kohteesta…",
"notifications_permission_banner.enable": "Ota työpöytäilmoitukset käyttöön", "notifications_permission_banner.enable": "Ota työpöytäilmoitukset käyttöön",
@ -662,10 +662,11 @@
"status.direct": "Mainitse @{name} yksityisesti", "status.direct": "Mainitse @{name} yksityisesti",
"status.direct_indicator": "Yksityinen maininta", "status.direct_indicator": "Yksityinen maininta",
"status.edit": "Muokkaa", "status.edit": "Muokkaa",
"status.edited": "Muokattu {date}", "status.edited": "Viimeksi muokattu {date}",
"status.edited_x_times": "Muokattu {count, plural, one {{count} kerran} other {{count} kertaa}}", "status.edited_x_times": "Muokattu {count, plural, one {{count} kerran} other {{count} kertaa}}",
"status.embed": "Upota", "status.embed": "Upota",
"status.favourite": "Suosikki", "status.favourite": "Suosikki",
"status.favourites": "{count, plural, one {suosikki} other {suosikkia}}",
"status.filter": "Suodata tämä julkaisu", "status.filter": "Suodata tämä julkaisu",
"status.filtered": "Suodatettu", "status.filtered": "Suodatettu",
"status.hide": "Piilota julkaisu", "status.hide": "Piilota julkaisu",
@ -686,6 +687,7 @@
"status.reblog": "Tehosta", "status.reblog": "Tehosta",
"status.reblog_private": "Tehosta alkuperäiselle yleisölle", "status.reblog_private": "Tehosta alkuperäiselle yleisölle",
"status.reblogged_by": "{name} tehosti", "status.reblogged_by": "{name} tehosti",
"status.reblogs": "{count, plural, one {tehostus} other {tehostusta}}",
"status.reblogs.empty": "Kukaan ei ole vielä tehostanut tätä julkaisua. Kun joku tekee niin, tulee hän tähän näkyviin.", "status.reblogs.empty": "Kukaan ei ole vielä tehostanut tätä julkaisua. Kun joku tekee niin, tulee hän tähän näkyviin.",
"status.redraft": "Poista ja palauta muokattavaksi", "status.redraft": "Poista ja palauta muokattavaksi",
"status.remove_bookmark": "Poista kirjanmerkki", "status.remove_bookmark": "Poista kirjanmerkki",

View file

@ -273,7 +273,6 @@
"status.direct": "Palihim na banggitin si/ang @{name}", "status.direct": "Palihim na banggitin si/ang @{name}",
"status.direct_indicator": "Palihim na banggit", "status.direct_indicator": "Palihim na banggit",
"status.edit": "Baguhin", "status.edit": "Baguhin",
"status.edited": "Binago noong {date}",
"status.edited_x_times": "Binago {count, plural, one {{count} beses} other {{count} na beses}}", "status.edited_x_times": "Binago {count, plural, one {{count} beses} other {{count} na beses}}",
"status.history.created": "Nilikha ni/ng {name} {date}", "status.history.created": "Nilikha ni/ng {name} {date}",
"status.history.edited": "Binago ni/ng {name} {date}", "status.history.edited": "Binago ni/ng {name} {date}",

View file

@ -662,10 +662,11 @@
"status.direct": "Umrøð @{name} privat", "status.direct": "Umrøð @{name} privat",
"status.direct_indicator": "Privat umrøða", "status.direct_indicator": "Privat umrøða",
"status.edit": "Rætta", "status.edit": "Rætta",
"status.edited": "Rættað {date}", "status.edited": "Seinast broytt {date}",
"status.edited_x_times": "Rættað {count, plural, one {{count} ferð} other {{count} ferð}}", "status.edited_x_times": "Rættað {count, plural, one {{count} ferð} other {{count} ferð}}",
"status.embed": "Legg inní", "status.embed": "Legg inní",
"status.favourite": "Dámdur postur", "status.favourite": "Dámdur postur",
"status.favourites": "{count, plural, one {yndispostur} other {yndispostar}}",
"status.filter": "Filtrera hendan postin", "status.filter": "Filtrera hendan postin",
"status.filtered": "Filtrerað", "status.filtered": "Filtrerað",
"status.hide": "Fjal post", "status.hide": "Fjal post",
@ -686,6 +687,7 @@
"status.reblog": "Stimbra", "status.reblog": "Stimbra",
"status.reblog_private": "Stimbra við upprunasýni", "status.reblog_private": "Stimbra við upprunasýni",
"status.reblogged_by": "{name} stimbrað", "status.reblogged_by": "{name} stimbrað",
"status.reblogs": "{count, plural, one {stimbran} other {stimbranir}}",
"status.reblogs.empty": "Eingin hevur stimbrað hendan postin enn. Tá onkur stimbrar postin, verður hann sjónligur her.", "status.reblogs.empty": "Eingin hevur stimbrað hendan postin enn. Tá onkur stimbrar postin, verður hann sjónligur her.",
"status.redraft": "Strika & ger nýggja kladdu", "status.redraft": "Strika & ger nýggja kladdu",
"status.remove_bookmark": "Gloym", "status.remove_bookmark": "Gloym",

View file

@ -646,7 +646,6 @@
"status.direct": "Mention privée @{name}", "status.direct": "Mention privée @{name}",
"status.direct_indicator": "Mention privée", "status.direct_indicator": "Mention privée",
"status.edit": "Modifier", "status.edit": "Modifier",
"status.edited": "Modifiée le {date}",
"status.edited_x_times": "Modifiée {count, plural, one {{count} fois} other {{count} fois}}", "status.edited_x_times": "Modifiée {count, plural, one {{count} fois} other {{count} fois}}",
"status.embed": "Intégrer", "status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris", "status.favourite": "Ajouter aux favoris",

View file

@ -646,7 +646,6 @@
"status.direct": "Mention privée @{name}", "status.direct": "Mention privée @{name}",
"status.direct_indicator": "Mention privée", "status.direct_indicator": "Mention privée",
"status.edit": "Modifier", "status.edit": "Modifier",
"status.edited": "Modifié le {date}",
"status.edited_x_times": "Modifié {count, plural, one {{count} fois} other {{count} fois}}", "status.edited_x_times": "Modifié {count, plural, one {{count} fois} other {{count} fois}}",
"status.embed": "Intégrer", "status.embed": "Intégrer",
"status.favourite": "Ajouter aux favoris", "status.favourite": "Ajouter aux favoris",

View file

@ -646,7 +646,6 @@
"status.direct": "Privee fermelde @{name}", "status.direct": "Privee fermelde @{name}",
"status.direct_indicator": "Priveefermelding", "status.direct_indicator": "Priveefermelding",
"status.edit": "Bewurkje", "status.edit": "Bewurkje",
"status.edited": "Bewurke op {date}",
"status.edited_x_times": "{count, plural, one {{count} kear} other {{count} kearen}} bewurke", "status.edited_x_times": "{count, plural, one {{count} kear} other {{count} kearen}} bewurke",
"status.embed": "Ynslute", "status.embed": "Ynslute",
"status.favourite": "Favoryt", "status.favourite": "Favoryt",

View file

@ -458,7 +458,6 @@
"status.copy": "Copy link to status", "status.copy": "Copy link to status",
"status.delete": "Scrios", "status.delete": "Scrios",
"status.edit": "Cuir in eagar", "status.edit": "Cuir in eagar",
"status.edited": "Curtha in eagar in {date}",
"status.edited_x_times": "Curtha in eagar {count, plural, one {{count} uair amháin} two {{count} uair} few {{count} uair} many {{count} uair} other {{count} uair}}", "status.edited_x_times": "Curtha in eagar {count, plural, one {{count} uair amháin} two {{count} uair} few {{count} uair} many {{count} uair} other {{count} uair}}",
"status.embed": "Leabaigh", "status.embed": "Leabaigh",
"status.filter": "Déan scagadh ar an bpostáil seo", "status.filter": "Déan scagadh ar an bpostáil seo",

View file

@ -646,7 +646,6 @@
"status.direct": "Thoir iomradh air @{name} gu prìobhaideach", "status.direct": "Thoir iomradh air @{name} gu prìobhaideach",
"status.direct_indicator": "Iomradh prìobhaideach", "status.direct_indicator": "Iomradh prìobhaideach",
"status.edit": "Deasaich", "status.edit": "Deasaich",
"status.edited": "Air a dheasachadh {date}",
"status.edited_x_times": "Chaidh a dheasachadh {count, plural, one {{counter} turas} two {{counter} thuras} few {{counter} tursan} other {{counter} turas}}", "status.edited_x_times": "Chaidh a dheasachadh {count, plural, one {{counter} turas} two {{counter} thuras} few {{counter} tursan} other {{counter} turas}}",
"status.embed": "Leabaich", "status.embed": "Leabaich",
"status.favourite": "Cuir ris na h-annsachdan", "status.favourite": "Cuir ris na h-annsachdan",

View file

@ -662,10 +662,11 @@
"status.direct": "Mencionar de xeito privado a @{name}", "status.direct": "Mencionar de xeito privado a @{name}",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edit": "Editar", "status.edit": "Editar",
"status.edited": "Editado {date}", "status.edited": "Última edición {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}", "status.edited_x_times": "Editado {count, plural, one {{count} vez} other {{count} veces}}",
"status.embed": "Incrustar", "status.embed": "Incrustar",
"status.favourite": "Favorecer", "status.favourite": "Favorecer",
"status.favourites": "{count, plural, one {favorecemento} other {favorecementos}}",
"status.filter": "Filtrar esta publicación", "status.filter": "Filtrar esta publicación",
"status.filtered": "Filtrado", "status.filtered": "Filtrado",
"status.hide": "Agochar publicación", "status.hide": "Agochar publicación",
@ -686,6 +687,7 @@
"status.reblog": "Promover", "status.reblog": "Promover",
"status.reblog_private": "Compartir coa audiencia orixinal", "status.reblog_private": "Compartir coa audiencia orixinal",
"status.reblogged_by": "{name} promoveu", "status.reblogged_by": "{name} promoveu",
"status.reblogs": "{count, plural, one {promoción} other {promocións}}",
"status.reblogs.empty": "Aínda ninguén promoveu esta publicación. Cando alguén o faga, amosarase aquí.", "status.reblogs.empty": "Aínda ninguén promoveu esta publicación. Cando alguén o faga, amosarase aquí.",
"status.redraft": "Eliminar e reescribir", "status.redraft": "Eliminar e reescribir",
"status.remove_bookmark": "Eliminar marcador", "status.remove_bookmark": "Eliminar marcador",

View file

@ -662,10 +662,11 @@
"status.direct": "הודעה פרטית אל @{name}", "status.direct": "הודעה פרטית אל @{name}",
"status.direct_indicator": "הודעה פרטית", "status.direct_indicator": "הודעה פרטית",
"status.edit": "עריכה", "status.edit": "עריכה",
"status.edited": "נערך ב{date}", "status.edited": "נערך לאחרונה {date}",
"status.edited_x_times": "נערך {count, plural, one {פעם {count}} other {{count} פעמים}}", "status.edited_x_times": "נערך {count, plural, one {פעם {count}} other {{count} פעמים}}",
"status.embed": "הטמעה", "status.embed": "הטמעה",
"status.favourite": "חיבוב", "status.favourite": "חיבוב",
"status.favourites": "{count, plural, one {חיבוב אחד} two {זוג חיבובים} other {# חיבובים}}",
"status.filter": "סנן הודעה זו", "status.filter": "סנן הודעה זו",
"status.filtered": "סונן", "status.filtered": "סונן",
"status.hide": "הסתרת חיצרוץ", "status.hide": "הסתרת חיצרוץ",
@ -686,6 +687,7 @@
"status.reblog": "הדהוד", "status.reblog": "הדהוד",
"status.reblog_private": "להדהד ברמת הנראות המקורית", "status.reblog_private": "להדהד ברמת הנראות המקורית",
"status.reblogged_by": "{name} הידהד/ה:", "status.reblogged_by": "{name} הידהד/ה:",
"status.reblogs": "{count, plural, one {הדהוד אחד} two {שני הדהודים} other {# הדהודים}}",
"status.reblogs.empty": "עוד לא הידהדו את ההודעה הזו. כאשר זה יקרה, ההדהודים יופיעו כאן.", "status.reblogs.empty": "עוד לא הידהדו את ההודעה הזו. כאשר זה יקרה, ההדהודים יופיעו כאן.",
"status.redraft": "מחיקה ועריכה מחדש", "status.redraft": "מחיקה ועריכה מחדש",
"status.remove_bookmark": "הסרת סימניה", "status.remove_bookmark": "הסרת סימניה",

View file

@ -475,7 +475,6 @@
"status.copy": "Copy link to status", "status.copy": "Copy link to status",
"status.delete": "Obriši", "status.delete": "Obriši",
"status.edit": "Uredi", "status.edit": "Uredi",
"status.edited": "Uređeno {date}",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.embed": "Umetni", "status.embed": "Umetni",
"status.filter": "Filtriraj ovu objavu", "status.filter": "Filtriraj ovu objavu",

View file

@ -662,10 +662,11 @@
"status.direct": "@{name} személyes említése", "status.direct": "@{name} személyes említése",
"status.direct_indicator": "Személyes említés", "status.direct_indicator": "Személyes említés",
"status.edit": "Szerkesztés", "status.edit": "Szerkesztés",
"status.edited": "Szerkesztve: {date}", "status.edited": "Utoljára szerkesztve {date}",
"status.edited_x_times": "{count, plural, one {{count} alkalommal} other {{count} alkalommal}} szerkesztve", "status.edited_x_times": "{count, plural, one {{count} alkalommal} other {{count} alkalommal}} szerkesztve",
"status.embed": "Beágyazás", "status.embed": "Beágyazás",
"status.favourite": "Kedvenc", "status.favourite": "Kedvenc",
"status.favourites": "{count, plural, one {kedvenc} other {kedvenc}}",
"status.filter": "E bejegyzés szűrése", "status.filter": "E bejegyzés szűrése",
"status.filtered": "Megszűrt", "status.filtered": "Megszűrt",
"status.hide": "Bejegyzés elrejtése", "status.hide": "Bejegyzés elrejtése",
@ -686,6 +687,7 @@
"status.reblog": "Megtolás", "status.reblog": "Megtolás",
"status.reblog_private": "Megtolás az eredeti közönségnek", "status.reblog_private": "Megtolás az eredeti közönségnek",
"status.reblogged_by": "{name} megtolta", "status.reblogged_by": "{name} megtolta",
"status.reblogs": "{count, plural, one {megtolás} other {megtolás}}",
"status.reblogs.empty": "Senki sem tolta még meg ezt a bejegyzést. Ha valaki megteszi, itt fog megjelenni.", "status.reblogs.empty": "Senki sem tolta még meg ezt a bejegyzést. Ha valaki megteszi, itt fog megjelenni.",
"status.redraft": "Törlés és újraírás", "status.redraft": "Törlés és újraírás",
"status.remove_bookmark": "Könyvjelző eltávolítása", "status.remove_bookmark": "Könyvjelző eltávolítása",

View file

@ -467,7 +467,6 @@
"status.direct": "Մասնաւոր յիշատակում @{name}", "status.direct": "Մասնաւոր յիշատակում @{name}",
"status.direct_indicator": "Մասնաւոր յիշատակում", "status.direct_indicator": "Մասնաւոր յիշատակում",
"status.edit": "Խմբագրել", "status.edit": "Խմբագրել",
"status.edited": "Խմբագրուել է՝ {date}",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.embed": "Ներդնել", "status.embed": "Ներդնել",
"status.favourite": "Հավանել", "status.favourite": "Հավանել",

View file

@ -366,7 +366,6 @@
"status.direct": "Mentionar privatemente a @{name}", "status.direct": "Mentionar privatemente a @{name}",
"status.direct_indicator": "Mention private", "status.direct_indicator": "Mention private",
"status.edit": "Modificar", "status.edit": "Modificar",
"status.edited": "Modificate le {date}",
"status.edited_x_times": "Modificate {count, plural, one {{count} tempore} other {{count} tempores}}", "status.edited_x_times": "Modificate {count, plural, one {{count} tempore} other {{count} tempores}}",
"status.favourite": "Adder al favoritos", "status.favourite": "Adder al favoritos",
"status.filter": "Filtrar iste message", "status.filter": "Filtrar iste message",

View file

@ -523,7 +523,6 @@
"status.delete": "Hapus", "status.delete": "Hapus",
"status.detailed_status": "Tampilan detail percakapan", "status.detailed_status": "Tampilan detail percakapan",
"status.edit": "Edit", "status.edit": "Edit",
"status.edited": "Diedit {date}",
"status.edited_x_times": "Diedit {count, plural, other {{count} kali}}", "status.edited_x_times": "Diedit {count, plural, other {{count} kali}}",
"status.embed": "Tanam", "status.embed": "Tanam",
"status.filter": "Saring kiriman ini", "status.filter": "Saring kiriman ini",

View file

@ -646,7 +646,6 @@
"status.direct": "Privatmen mentionar @{name}", "status.direct": "Privatmen mentionar @{name}",
"status.direct_indicator": "Privat mention", "status.direct_indicator": "Privat mention",
"status.edit": "Modificar", "status.edit": "Modificar",
"status.edited": "Modificat ye {date}",
"status.edited_x_times": "Modificat {count, plural, one {{count} vez} other {{count} vezes}}", "status.edited_x_times": "Modificat {count, plural, one {{count} vez} other {{count} vezes}}",
"status.embed": "Inbedar", "status.embed": "Inbedar",
"status.favourite": "Favoritisar", "status.favourite": "Favoritisar",

View file

@ -610,7 +610,6 @@
"status.direct": "Private mencionez @{name}", "status.direct": "Private mencionez @{name}",
"status.direct_indicator": "Privata menciono", "status.direct_indicator": "Privata menciono",
"status.edit": "Modifikez", "status.edit": "Modifikez",
"status.edited": "Modifikesis ye {date}",
"status.edited_x_times": "Modifikesis {count, plural, one {{count} foyo} other {{count} foyi}}", "status.edited_x_times": "Modifikesis {count, plural, one {{count} foyo} other {{count} foyi}}",
"status.embed": "Eninsertez", "status.embed": "Eninsertez",
"status.favourite": "Favorizar", "status.favourite": "Favorizar",

View file

@ -662,10 +662,11 @@
"status.direct": "Einkaspjall við @{name}", "status.direct": "Einkaspjall við @{name}",
"status.direct_indicator": "Einkaspjall", "status.direct_indicator": "Einkaspjall",
"status.edit": "Breyta", "status.edit": "Breyta",
"status.edited": "Breytt {date}", "status.edited": "Síðast breytt {date}",
"status.edited_x_times": "Breytt {count, plural, one {{count} sinni} other {{count} sinnum}}", "status.edited_x_times": "Breytt {count, plural, one {{count} sinni} other {{count} sinnum}}",
"status.embed": "Ívefja", "status.embed": "Ívefja",
"status.favourite": "Eftirlæti", "status.favourite": "Eftirlæti",
"status.favourites": "{count, plural, one {eftirlæti} other {eftirlæti}}",
"status.filter": "Sía þessa færslu", "status.filter": "Sía þessa færslu",
"status.filtered": "Síað", "status.filtered": "Síað",
"status.hide": "Fela færslu", "status.hide": "Fela færslu",
@ -686,6 +687,7 @@
"status.reblog": "Endurbirting", "status.reblog": "Endurbirting",
"status.reblog_private": "Endurbirta til upphaflegra lesenda", "status.reblog_private": "Endurbirta til upphaflegra lesenda",
"status.reblogged_by": "{name} endurbirti", "status.reblogged_by": "{name} endurbirti",
"status.reblogs": "{count, plural, one {endurbirting} other {endurbirtingar}}",
"status.reblogs.empty": "Enginn hefur ennþá endurbirt þessa færslu. Þegar einhver gerir það, mun það birtast hér.", "status.reblogs.empty": "Enginn hefur ennþá endurbirt þessa færslu. Þegar einhver gerir það, mun það birtast hér.",
"status.redraft": "Eyða og endurvinna drög", "status.redraft": "Eyða og endurvinna drög",
"status.remove_bookmark": "Fjarlægja bókamerki", "status.remove_bookmark": "Fjarlægja bókamerki",

View file

@ -662,10 +662,11 @@
"status.direct": "Menziona privatamente @{name}", "status.direct": "Menziona privatamente @{name}",
"status.direct_indicator": "Menzione privata", "status.direct_indicator": "Menzione privata",
"status.edit": "Modifica", "status.edit": "Modifica",
"status.edited": "Modificato il {date}", "status.edited": "Ultima modifica {date}",
"status.edited_x_times": "Modificato {count, plural, one {{count} volta} other {{count} volte}}", "status.edited_x_times": "Modificato {count, plural, one {{count} volta} other {{count} volte}}",
"status.embed": "Incorpora", "status.embed": "Incorpora",
"status.favourite": "Preferito", "status.favourite": "Preferito",
"status.favourites": "{count, plural, one {preferito} other {preferiti}}",
"status.filter": "Filtra questo post", "status.filter": "Filtra questo post",
"status.filtered": "Filtrato", "status.filtered": "Filtrato",
"status.hide": "Nascondi il post", "status.hide": "Nascondi il post",
@ -686,6 +687,7 @@
"status.reblog": "Reblog", "status.reblog": "Reblog",
"status.reblog_private": "Reblog con visibilità originale", "status.reblog_private": "Reblog con visibilità originale",
"status.reblogged_by": "Rebloggato da {name}", "status.reblogged_by": "Rebloggato da {name}",
"status.reblogs": "{count, plural, one {boost} other {boost}}",
"status.reblogs.empty": "Ancora nessuno ha rebloggato questo post. Quando qualcuno lo farà, apparirà qui.", "status.reblogs.empty": "Ancora nessuno ha rebloggato questo post. Quando qualcuno lo farà, apparirà qui.",
"status.redraft": "Elimina e riscrivi", "status.redraft": "Elimina e riscrivi",
"status.remove_bookmark": "Rimuovi segnalibro", "status.remove_bookmark": "Rimuovi segnalibro",

View file

@ -241,6 +241,7 @@
"empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。", "empty_column.list": "このリストにはまだなにもありません。このリストのメンバーが新しい投稿をするとここに表示されます。",
"empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。", "empty_column.lists": "まだリストがありません。リストを作るとここに表示されます。",
"empty_column.mutes": "まだ誰もミュートしていません。", "empty_column.mutes": "まだ誰もミュートしていません。",
"empty_column.notification_requests": "ここに表示するものはありません。新しい通知を受け取ったとき、フィルタリング設定で通知がブロックされたアカウントがある場合はここに表示されます。",
"empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。", "empty_column.notifications": "まだ通知がありません。他の人とふれ合って会話を始めましょう。",
"empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう", "empty_column.public": "ここにはまだ何もありません! 公開で何かを投稿したり、他のサーバーのユーザーをフォローしたりしていっぱいにしましょう",
"error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。", "error.unexpected_crash.explanation": "不具合かブラウザの互換性問題のため、このページを正しく表示できませんでした。",
@ -271,6 +272,8 @@
"filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します", "filter_modal.select_filter.subtitle": "既存のカテゴリーを使用するか新規作成します",
"filter_modal.select_filter.title": "この投稿をフィルターする", "filter_modal.select_filter.title": "この投稿をフィルターする",
"filter_modal.title.status": "投稿をフィルターする", "filter_modal.title.status": "投稿をフィルターする",
"filtered_notifications_banner.pending_requests": "{count, plural, =0 {アカウント} other {#アカウント}}からの通知がブロックされています",
"filtered_notifications_banner.title": "ブロック済みの通知",
"firehose.all": "すべて", "firehose.all": "すべて",
"firehose.local": "このサーバー", "firehose.local": "このサーバー",
"firehose.remote": "ほかのサーバー", "firehose.remote": "ほかのサーバー",
@ -439,6 +442,10 @@
"notification.reblog": "{name}さんがあなたの投稿をブーストしました", "notification.reblog": "{name}さんがあなたの投稿をブーストしました",
"notification.status": "{name}さんが投稿しました", "notification.status": "{name}さんが投稿しました",
"notification.update": "{name}さんが投稿を編集しました", "notification.update": "{name}さんが投稿を編集しました",
"notification_requests.accept": "受け入れる",
"notification_requests.dismiss": "無視",
"notification_requests.notifications_from": "{name}からの通知",
"notification_requests.title": "ブロック済みの通知",
"notifications.clear": "通知を消去", "notifications.clear": "通知を消去",
"notifications.clear_confirmation": "本当に通知を消去しますか?", "notifications.clear_confirmation": "本当に通知を消去しますか?",
"notifications.column_settings.admin.report": "新しい通報:", "notifications.column_settings.admin.report": "新しい通報:",
@ -470,6 +477,15 @@
"notifications.permission_denied": "ブラウザの通知が拒否されているためデスクトップ通知は利用できません", "notifications.permission_denied": "ブラウザの通知が拒否されているためデスクトップ通知は利用できません",
"notifications.permission_denied_alert": "ブラウザの通知が拒否されているためデスクトップ通知を有効にできません", "notifications.permission_denied_alert": "ブラウザの通知が拒否されているためデスクトップ通知を有効にできません",
"notifications.permission_required": "必要な権限が付与されていないため、デスクトップ通知は利用できません。", "notifications.permission_required": "必要な権限が付与されていないため、デスクトップ通知は利用できません。",
"notifications.policy.filter_new_accounts.hint": "作成から{days, plural, other {#日}}以内のアカウントからの通知がブロックされます",
"notifications.policy.filter_new_accounts_title": "新しいアカウントからの通知をブロックする",
"notifications.policy.filter_not_followers_hint": "フォローされていても、フォローから{days, plural, other {#日}}経っていない場合はブロックされます",
"notifications.policy.filter_not_followers_title": "フォローされていないアカウントからの通知をブロックする",
"notifications.policy.filter_not_following_hint": "手動で受け入れたアカウントはブロックされません",
"notifications.policy.filter_not_following_title": "フォローしていないアカウントからの通知をブロックする",
"notifications.policy.filter_private_mentions_hint": "あなたがメンションした相手からの返信、およびフォローしているアカウントからの返信以外がブロックされます",
"notifications.policy.filter_private_mentions_title": "外部からの非公開の返信をブロックする",
"notifications.policy.title": "通知のフィルタリング",
"notifications_permission_banner.enable": "デスクトップ通知を有効にする", "notifications_permission_banner.enable": "デスクトップ通知を有効にする",
"notifications_permission_banner.how_to_control": "Mastodonを閉じている間でも通知を受信するにはデスクトップ通知を有効にしてください。有効にすると上の {icon} ボタンから通知の内容を細かくカスタマイズできます。", "notifications_permission_banner.how_to_control": "Mastodonを閉じている間でも通知を受信するにはデスクトップ通知を有効にしてください。有効にすると上の {icon} ボタンから通知の内容を細かくカスタマイズできます。",
"notifications_permission_banner.title": "お見逃しなく", "notifications_permission_banner.title": "お見逃しなく",
@ -646,7 +662,6 @@
"status.direct": "@{name}さんに非公開で投稿", "status.direct": "@{name}さんに非公開で投稿",
"status.direct_indicator": "非公開の返信", "status.direct_indicator": "非公開の返信",
"status.edit": "編集", "status.edit": "編集",
"status.edited": "{date}に編集",
"status.edited_x_times": "{count}回編集", "status.edited_x_times": "{count}回編集",
"status.embed": "埋め込み", "status.embed": "埋め込み",
"status.favourite": "お気に入り", "status.favourite": "お気に入り",

Some files were not shown because too many files have changed in this diff Show more