Merge remote-tracking branch 'soapbox/develop' into familiar-followers

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-develop-3zknud/deployments/28^2
marcin mikołajczak 2 years ago
commit 2ec9cacb27

@ -57,6 +57,10 @@ export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST';
export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS';
export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL';
export const ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST';
export const ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS';
export const ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL = 'ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL';
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS';
export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL';
@ -520,6 +524,42 @@ export function unsubscribeAccountFail(error) {
};
}
export function removeFromFollowers(id) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/remove_from_followers`).then(response => {
dispatch(removeFromFollowersSuccess(response.data));
}).catch(error => {
dispatch(removeFromFollowersFail(id, error));
});
};
}
export function removeFromFollowersRequest(id) {
return {
type: ACCOUNT_REMOVE_FROM_FOLLOWERS_REQUEST,
id,
};
}
export function removeFromFollowersSuccess(relationship) {
return {
type: ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS,
relationship,
};
}
export function removeFromFollowersFail(error) {
return {
type: ACCOUNT_REMOVE_FROM_FOLLOWERS_FAIL,
error,
};
}
export function fetchFollowers(id) {
return (dispatch, getState) => {
dispatch(fetchFollowersRequest(id));

@ -1,104 +0,0 @@
import { defineMessages } from 'react-intl';
import snackbar from 'soapbox/actions/snackbar';
import api, { getLinks } from '../api';
export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
export const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS';
export const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL';
export const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST';
export const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS';
export const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL';
export const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST';
export const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS';
export const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL';
const messages = defineMessages({
blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' },
followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' },
mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' },
});
function fileExport(content, fileName) {
const fileToDownload = document.createElement('a');
fileToDownload.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(content));
fileToDownload.setAttribute('download', fileName);
fileToDownload.style.display = 'none';
document.body.appendChild(fileToDownload);
fileToDownload.click();
document.body.removeChild(fileToDownload);
}
function listAccounts(state) {
return async apiResponse => {
const followings = apiResponse.data;
let accounts = [];
let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
while (next) {
apiResponse = await api(state).get(next.uri);
next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
Array.prototype.push.apply(followings, apiResponse.data);
}
accounts = followings.map(account => account.fqn);
return [... new Set(accounts)];
};
}
export function exportFollows(intl) {
return (dispatch, getState) => {
dispatch({ type: EXPORT_FOLLOWS_REQUEST });
const me = getState().get('me');
return api(getState)
.get(`/api/v1/accounts/${me}/following?limit=40`)
.then(listAccounts(getState))
.then((followings) => {
followings = followings.map(fqn => fqn + ',true');
followings.unshift('Account address,Show boosts');
fileExport(followings.join('\n'), 'export_followings.csv');
dispatch(snackbar.success(intl.formatMessage(messages.followersSuccess)));
dispatch({ type: EXPORT_FOLLOWS_SUCCESS });
}).catch(error => {
dispatch({ type: EXPORT_FOLLOWS_FAIL, error });
});
};
}
export function exportBlocks(intl) {
return (dispatch, getState) => {
dispatch({ type: EXPORT_BLOCKS_REQUEST });
return api(getState)
.get('/api/v1/blocks?limit=40')
.then(listAccounts(getState))
.then((blocks) => {
fileExport(blocks.join('\n'), 'export_block.csv');
dispatch(snackbar.success(intl.formatMessage(messages.blocksSuccess)));
dispatch({ type: EXPORT_BLOCKS_SUCCESS });
}).catch(error => {
dispatch({ type: EXPORT_BLOCKS_FAIL, error });
});
};
}
export function exportMutes(intl) {
return (dispatch, getState) => {
dispatch({ type: EXPORT_MUTES_REQUEST });
return api(getState)
.get('/api/v1/mutes?limit=40')
.then(listAccounts(getState))
.then((mutes) => {
fileExport(mutes.join('\n'), 'export_mutes.csv');
dispatch(snackbar.success(intl.formatMessage(messages.mutesSuccess)));
dispatch({ type: EXPORT_MUTES_SUCCESS });
}).catch(error => {
dispatch({ type: EXPORT_MUTES_FAIL, error });
});
};
}

@ -0,0 +1,113 @@
import { defineMessages } from 'react-intl';
import api, { getLinks } from '../api';
import snackbar from './snackbar';
import type { SnackbarAction } from './snackbar';
import type { AxiosResponse } from 'axios';
import type { RootState } from 'soapbox/store';
export const EXPORT_FOLLOWS_REQUEST = 'EXPORT_FOLLOWS_REQUEST';
export const EXPORT_FOLLOWS_SUCCESS = 'EXPORT_FOLLOWS_SUCCESS';
export const EXPORT_FOLLOWS_FAIL = 'EXPORT_FOLLOWS_FAIL';
export const EXPORT_BLOCKS_REQUEST = 'EXPORT_BLOCKS_REQUEST';
export const EXPORT_BLOCKS_SUCCESS = 'EXPORT_BLOCKS_SUCCESS';
export const EXPORT_BLOCKS_FAIL = 'EXPORT_BLOCKS_FAIL';
export const EXPORT_MUTES_REQUEST = 'EXPORT_MUTES_REQUEST';
export const EXPORT_MUTES_SUCCESS = 'EXPORT_MUTES_SUCCESS';
export const EXPORT_MUTES_FAIL = 'EXPORT_MUTES_FAIL';
const messages = defineMessages({
blocksSuccess: { id: 'export_data.success.blocks', defaultMessage: 'Blocks exported successfully' },
followersSuccess: { id: 'export_data.success.followers', defaultMessage: 'Followers exported successfully' },
mutesSuccess: { id: 'export_data.success.mutes', defaultMessage: 'Mutes exported successfully' },
});
type ExportDataActions = {
type: typeof EXPORT_FOLLOWS_REQUEST
| typeof EXPORT_FOLLOWS_SUCCESS
| typeof EXPORT_FOLLOWS_FAIL
| typeof EXPORT_BLOCKS_REQUEST
| typeof EXPORT_BLOCKS_SUCCESS
| typeof EXPORT_BLOCKS_FAIL
| typeof EXPORT_MUTES_REQUEST
| typeof EXPORT_MUTES_SUCCESS
| typeof EXPORT_MUTES_FAIL,
error?: any,
} | SnackbarAction
function fileExport(content: string, fileName: string) {
const fileToDownload = document.createElement('a');
fileToDownload.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(content));
fileToDownload.setAttribute('download', fileName);
fileToDownload.style.display = 'none';
document.body.appendChild(fileToDownload);
fileToDownload.click();
document.body.removeChild(fileToDownload);
}
const listAccounts = (getState: () => RootState) => async(apiResponse: AxiosResponse<any, any>) => {
const followings = apiResponse.data;
let accounts = [];
let next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
while (next) {
apiResponse = await api(getState).get(next.uri);
next = getLinks(apiResponse).refs.find(link => link.rel === 'next');
Array.prototype.push.apply(followings, apiResponse.data);
}
accounts = followings.map((account: { fqn: string }) => account.fqn);
return Array.from(new Set(accounts));
};
export const exportFollows = () => (dispatch: React.Dispatch<ExportDataActions>, getState: () => RootState) => {
dispatch({ type: EXPORT_FOLLOWS_REQUEST });
const me = getState().me;
return api(getState)
.get(`/api/v1/accounts/${me}/following?limit=40`)
.then(listAccounts(getState))
.then((followings) => {
followings = followings.map(fqn => fqn + ',true');
followings.unshift('Account address,Show boosts');
fileExport(followings.join('\n'), 'export_followings.csv');
dispatch(snackbar.success(messages.followersSuccess));
dispatch({ type: EXPORT_FOLLOWS_SUCCESS });
}).catch(error => {
dispatch({ type: EXPORT_FOLLOWS_FAIL, error });
});
};
export const exportBlocks = () => (dispatch: React.Dispatch<ExportDataActions>, getState: () => RootState) => {
dispatch({ type: EXPORT_BLOCKS_REQUEST });
return api(getState)
.get('/api/v1/blocks?limit=40')
.then(listAccounts(getState))
.then((blocks) => {
fileExport(blocks.join('\n'), 'export_block.csv');
dispatch(snackbar.success(messages.blocksSuccess));
dispatch({ type: EXPORT_BLOCKS_SUCCESS });
}).catch(error => {
dispatch({ type: EXPORT_BLOCKS_FAIL, error });
});
};
export const exportMutes = () => (dispatch: React.Dispatch<ExportDataActions>, getState: () => RootState) => {
dispatch({ type: EXPORT_MUTES_REQUEST });
return api(getState)
.get('/api/v1/mutes?limit=40')
.then(listAccounts(getState))
.then((mutes) => {
fileExport(mutes.join('\n'), 'export_mutes.csv');
dispatch(snackbar.success(messages.mutesSuccess));
dispatch({ type: EXPORT_MUTES_SUCCESS });
}).catch(error => {
dispatch({ type: EXPORT_MUTES_FAIL, error });
});
};

@ -4,6 +4,9 @@ import snackbar from 'soapbox/actions/snackbar';
import api from '../api';
import type { SnackbarAction } from './snackbar';
import type { RootState } from 'soapbox/store';
export const IMPORT_FOLLOWS_REQUEST = 'IMPORT_FOLLOWS_REQUEST';
export const IMPORT_FOLLOWS_SUCCESS = 'IMPORT_FOLLOWS_SUCCESS';
export const IMPORT_FOLLOWS_FAIL = 'IMPORT_FOLLOWS_FAIL';
@ -16,50 +19,61 @@ export const IMPORT_MUTES_REQUEST = 'IMPORT_MUTES_REQUEST';
export const IMPORT_MUTES_SUCCESS = 'IMPORT_MUTES_SUCCESS';
export const IMPORT_MUTES_FAIL = 'IMPORT_MUTES_FAIL';
type ImportDataActions = {
type: typeof IMPORT_FOLLOWS_REQUEST
| typeof IMPORT_FOLLOWS_SUCCESS
| typeof IMPORT_FOLLOWS_FAIL
| typeof IMPORT_BLOCKS_REQUEST
| typeof IMPORT_BLOCKS_SUCCESS
| typeof IMPORT_BLOCKS_FAIL
| typeof IMPORT_MUTES_REQUEST
| typeof IMPORT_MUTES_SUCCESS
| typeof IMPORT_MUTES_FAIL,
error?: any,
config?: string
} | SnackbarAction
const messages = defineMessages({
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' },
mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' },
});
export function importFollows(intl, params) {
return (dispatch, getState) => {
export const importFollows = (params: FormData) =>
(dispatch: React.Dispatch<ImportDataActions>, getState: () => RootState) => {
dispatch({ type: IMPORT_FOLLOWS_REQUEST });
return api(getState)
.post('/api/pleroma/follow_import', params)
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.followersSuccess)));
dispatch(snackbar.success(messages.followersSuccess));
dispatch({ type: IMPORT_FOLLOWS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_FOLLOWS_FAIL, error });
});
};
}
export function importBlocks(intl, params) {
return (dispatch, getState) => {
export const importBlocks = (params: FormData) =>
(dispatch: React.Dispatch<ImportDataActions>, getState: () => RootState) => {
dispatch({ type: IMPORT_BLOCKS_REQUEST });
return api(getState)
.post('/api/pleroma/blocks_import', params)
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.blocksSuccess)));
dispatch(snackbar.success(messages.blocksSuccess));
dispatch({ type: IMPORT_BLOCKS_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_BLOCKS_FAIL, error });
});
};
}
export function importMutes(intl, params) {
return (dispatch, getState) => {
export const importMutes = (params: FormData) =>
(dispatch: React.Dispatch<ImportDataActions>, getState: () => RootState) => {
dispatch({ type: IMPORT_MUTES_REQUEST });
return api(getState)
.post('/api/pleroma/mutes_import', params)
.then(response => {
dispatch(snackbar.success(intl.formatMessage(messages.mutesSuccess)));
dispatch(snackbar.success(messages.mutesSuccess));
dispatch({ type: IMPORT_MUTES_SUCCESS, config: response.data });
}).catch(error => {
dispatch({ type: IMPORT_MUTES_FAIL, error });
});
};
}

@ -1,28 +0,0 @@
import { ALERT_SHOW } from './alerts';
export const show = (severity, message, actionLabel, actionLink) => ({
type: ALERT_SHOW,
message,
actionLabel,
actionLink,
severity,
});
export function info(message, actionLabel, actionLink) {
return show('info', message, actionLabel, actionLink);
}
export function success(message, actionLabel, actionLink) {
return show('success', message, actionLabel, actionLink);
}
export function error(message, actionLabel, actionLink) {
return show('error', message, actionLabel, actionLink);
}
export default {
info,
success,
error,
show,
};

@ -0,0 +1,39 @@
import { ALERT_SHOW } from './alerts';
import type { MessageDescriptor } from 'react-intl';
type SnackbarActionSeverity = 'info' | 'success' | 'error'
type SnackbarMessage = string | MessageDescriptor
export type SnackbarAction = {
type: typeof ALERT_SHOW
message: SnackbarMessage
actionLabel?: string
actionLink?: string
severity: SnackbarActionSeverity
}
export const show = (severity: SnackbarActionSeverity, message: SnackbarMessage, actionLabel?: string, actionLink?: string): SnackbarAction => ({
type: ALERT_SHOW,
message,
actionLabel,
actionLink,
severity,
});
export const info = (message: SnackbarMessage, actionLabel?: string, actionLink?: string) =>
show('info', message, actionLabel, actionLink);
export const success = (message: SnackbarMessage, actionLabel?: string, actionLink?: string) =>
show('success', message, actionLabel, actionLink);
export const error = (message: SnackbarMessage, actionLabel?: string, actionLink?: string) =>
show('error', message, actionLabel, actionLink);
export default {
info,
success,
error,
show,
};

@ -1,7 +1,7 @@
import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import api from '../api';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
@ -32,11 +32,17 @@ export function fetchSuggestionsV1(params = {}) {
export function fetchSuggestionsV2(params = {}) {
return (dispatch, getState) => {
const next = getState().getIn(['suggestions', 'next']);
dispatch({ type: SUGGESTIONS_V2_FETCH_REQUEST, skipLoading: true });
return api(getState).get('/api/v2/suggestions', { params }).then(({ data: suggestions }) => {
return api(getState).get(next ? next.uri : '/api/v2/suggestions', next ? {} : { params }).then((response) => {
const suggestions = response.data;
const accounts = suggestions.map(({ account }) => account);
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(accounts));
dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, skipLoading: true });
dispatch({ type: SUGGESTIONS_V2_FETCH_SUCCESS, suggestions, next, skipLoading: true });
return suggestions;
}).catch(error => {
dispatch({ type: SUGGESTIONS_V2_FETCH_FAIL, error, skipLoading: true, skipAlert: true });

@ -244,7 +244,9 @@ function checkEmailAvailability(email) {
return api(getState).get(`/api/v1/pepe/account/exists?email=${email}`, {
headers: { Authorization: `Bearer ${token}` },
}).finally(() => dispatch({ type: SET_LOADING, value: false }));
})
.catch(() => {})
.then(() => dispatch({ type: SET_LOADING, value: false }));
};
}

@ -42,6 +42,8 @@ interface IScrollableList extends VirtuosoProps<any, any> {
onRefresh?: () => Promise<any>,
className?: string,
itemClassName?: string,
style?: React.CSSProperties,
useWindowScroll?: boolean
}
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
@ -63,6 +65,8 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
placeholderCount = 0,
initialTopMostItemIndex = 0,
scrollerRef,
style = {},
useWindowScroll = true,
}, ref) => {
const settings = useSettings();
const autoloadMore = settings.get('autoloadMore');
@ -129,7 +133,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
const renderFeed = (): JSX.Element => (
<Virtuoso
ref={ref}
useWindowScroll
useWindowScroll={useWindowScroll}
className={className}
data={data}
startReached={onScrollToTop}
@ -137,6 +141,7 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
isScrolling={isScrolling => isScrolling && onScroll && onScroll()}
itemContent={renderItem}
initialTopMostItemIndex={showLoading ? 0 : initialTopMostItemIndex}
style={style}
context={{
listClassName: className,
itemClassName,

@ -1,8 +1,9 @@
import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl';
import { FormattedList, FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
@ -42,7 +43,7 @@ class StatusReplyMentions extends ImmutablePureComponent {
return null;
}
const to = status.get('mentions', []);
const to = status.get('mentions', ImmutableList());
// The post is a reply, but it has no mentions.
// Rare, but it can happen.
@ -58,23 +59,27 @@ class StatusReplyMentions extends ImmutablePureComponent {
}
// The typical case with a reply-to and a list of mentions.
const accounts = to.slice(0, 2).map(account => (
<HoverRefWrapper accountId={account.get('id')} inline>
<Link to={`/@${account.get('acct')}`} className='reply-mentions__account'>@{account.get('username')}</Link>
</HoverRefWrapper>
)).toArray();
if (to.size > 2) {
accounts.push(
<span className='hover:underline cursor-pointer' role='presentation' onClick={this.handleOpenMentionsModal}>
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />
</span>,
);
}
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
defaultMessage='Replying to {accounts}'
values={{
accounts: to.slice(0, 2).map((account) => (<>
<HoverRefWrapper accountId={account.get('id')} inline>
<Link to={`/@${account.get('acct')}`} className='reply-mentions__account'>@{account.get('username')}</Link>
</HoverRefWrapper>
{' '}
</>)),
more: to.size > 2 && (
<span className='hover:underline cursor-pointer' role='presentation' onClick={this.handleOpenMentionsModal}>
<FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />
</span>
),
accounts: <FormattedList type='conjunction' value={accounts} />,
}}
/>
</div>

@ -6,7 +6,7 @@ import { Text } from 'soapbox/components/ui';
/** Represents a deleted item. */
const Tombstone: React.FC = () => {
return (
<div className='my-4 p-9 flex items-center justify-center sm:rounded-xl bg-gray-100 border border-solid border-gray-200 dark:bg-slate-900 dark:border-slate-700'>
<div className='p-9 flex items-center justify-center sm:rounded-xl bg-gray-100 border border-solid border-gray-200 dark:bg-slate-900 dark:border-slate-700'>
<Text>
<FormattedMessage id='statuses.tombstone' defaultMessage='One or more posts is unavailable.' />
</Text>

@ -1,9 +1,9 @@
/**
* iOS style loading spinner.
* Adapted from: https://loading.io/css/
* With some help scaling it: https://signalvnoise.com/posts/2577-loading-spinner-animation-using-css-and-webkit
*/
.spinner {
@apply inline-block relative w-20 h-20;
}

@ -6,7 +6,7 @@ import Text from '../text/text';
import './spinner.css';
interface ILoadingIndicator {
interface ISpinner {
/** Width and height of the spinner in pixels. */
size?: number,
/** Whether to display "Loading..." beneath the spinner. */
@ -14,7 +14,7 @@ interface ILoadingIndicator {
}
/** Spinning loading placeholder. */
const LoadingIndicator = ({ size = 30, withText = true }: ILoadingIndicator) => (
const Spinner = ({ size = 30, withText = true }: ISpinner) => (
<Stack space={2} justifyContent='center' alignItems='center'>
<div className='spinner' style={{ width: size, height: size }}>
{Array.from(Array(12).keys()).map(i => (
@ -30,4 +30,4 @@ const LoadingIndicator = ({ size = 30, withText = true }: ILoadingIndicator) =>
</Stack>
);
export default LoadingIndicator;
export default Spinner;

@ -14,6 +14,7 @@ import { loadSoapboxConfig, getSoapboxConfig } from 'soapbox/actions/soapbox';
import { fetchVerificationConfig } from 'soapbox/actions/verification';
import * as BuildConfig from 'soapbox/build_config';
import Helmet from 'soapbox/components/helmet';
import { Spinner } from 'soapbox/components/ui';
import AuthLayout from 'soapbox/features/auth_layout';
import OnboardingWizard from 'soapbox/features/onboarding/onboarding-wizard';
import PublicLayout from 'soapbox/features/public_layout';
@ -115,10 +116,25 @@ const SoapboxMount = () => {
return !(location.state?.soapboxModalKey && location.state?.soapboxModalKey !== prevRouterProps?.location?.state?.soapboxModalKey);
};
if (me === null) return null;
if (me && !account) return null;
if (!isLoaded) return null;
if (localeLoading) return null;
/** Whether to display a loading indicator. */
const showLoading = [
me === null,
me && !account,
!isLoaded,
localeLoading,
].some(Boolean);
if (showLoading) {
return (
<div className='p-4 h-screen w-screen flex items-center justify-center'>
<Helmet>
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
</Helmet>
<Spinner size={40} withText={false} />
</div>
);
}
const waitlisted = account && !account.source.get('approved', true);

@ -48,6 +48,7 @@ const messages = defineMessages({
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
removeFromFollowers: { id: 'account.remove_from_followers', defaultMessage: 'Remove this follower' },
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' },
@ -283,6 +284,14 @@ class Header extends ImmutablePureComponent {
});
}
if (features.removeFromFollowers && account.getIn(['relationship', 'followed_by'])) {
menu.push({
text: intl.formatMessage(messages.removeFromFollowers),
action: this.props.onRemoveFromFollowers,
icon: require('@tabler/icons/icons/user-x.svg'),
});
}
if (account.getIn(['relationship', 'muting'])) {
menu.push({
text: intl.formatMessage(messages.unmute, { name: account.get('username') }),

@ -25,6 +25,7 @@ class Header extends ImmutablePureComponent {
onUnblockDomain: PropTypes.func.isRequired,
onEndorseToggle: PropTypes.func.isRequired,
onAddToList: PropTypes.func.isRequired,
onRemoveFromFollowers: PropTypes.func.isRequired,
username: PropTypes.string,
history: PropTypes.object,
};
@ -141,6 +142,10 @@ class Header extends ImmutablePureComponent {
this.props.onShowNote(this.props.account);
}
handleRemoveFromFollowers = () => {
this.props.onRemoveFromFollowers(this.props.account);
}
render() {
const { account } = this.props;
const moved = (account) ? account.get('moved') : false;
@ -177,6 +182,7 @@ class Header extends ImmutablePureComponent {
onSuggestUser={this.handleSuggestUser}
onUnsuggestUser={this.handleUnsuggestUser}
onShowNote={this.handleShowNote}
onRemoveFromFollowers={this.handleRemoveFromFollowers}
username={this.props.username}
/>
</>

@ -13,6 +13,7 @@ import {
unpinAccount,
subscribeAccount,
unsubscribeAccount,
removeFromFollowers,
} from 'soapbox/actions/accounts';
import {
verifyUser,
@ -56,6 +57,7 @@ const messages = defineMessages({
demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' },
});
const makeMapStateToProps = () => {
@ -269,6 +271,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onShowNote(account) {
dispatch(initAccountNoteModal(account));
},
onRemoveFromFollowers(account) {
dispatch((_, getState) => {
const unfollowModal = getSettings(getState()).get('unfollowModal');
if (unfollowModal) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.remove_from_followers.message' defaultMessage='Are you sure you want to remove {name} from your followers?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.removeFromFollowersConfirm),
onConfirm: () => dispatch(removeFromFollowers(account.get('id'))),
}));
} else {
dispatch(removeFromFollowers(account.get('id')));
}
});
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

@ -40,23 +40,23 @@ const Search: React.FC = () => {
const hasValue = value.length > 0;
return (
<div className='aliases_search search'>
<label>
<div className='flex items-center gap-1'>
<label className='flex-grow relative'>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
<input
className='search__input'
className='block w-full sm:text-sm dark:bg-slate-800 dark:text-white dark:placeholder:text-gray-500 focus:ring-indigo-500 focus:border-indigo-500 rounded-full'
type='text'
value={value}
onChange={handleChange}
onKeyUp={handleKeyUp}
placeholder={intl.formatMessage(messages.search)}
/>
</label>
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
</div>
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
<Icon src={require('@tabler/icons/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
</div>
</label>
<Button onClick={handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
</div>
);

@ -1,5 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedList, FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { openModal } from 'soapbox/actions/modals';
@ -47,14 +47,23 @@ const ReplyMentions: React.FC = () => {
);
}
const accounts = to.slice(0, 2).map((acct: string) => (
<span className='reply-mentions__account'>@{acct.split('@')[0]}</span>
)).toArray();
if (to.size > 2) {
accounts.push(
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />,
);
}
return (
<a href='#' className='reply-mentions' onClick={handleClick}>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
defaultMessage='Replying to {accounts}'
values={{
accounts: to.slice(0, 2).map((acct: string) => <><span className='reply-mentions__account'>@{acct.split('@')[0]}</span>{' '}</>),
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
accounts: <FormattedList type='conjunction' value={accounts} />,
}}
/>
</a>

@ -1,54 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Button, Form, FormActions, Text } from 'soapbox/components/ui';
export default @connect()
@injectIntl
class CSVExporter extends ImmutablePureComponent {
static propTypes = {
action: PropTypes.func.isRequired,
messages: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
isLoading: false,
}
handleClick = (event) => {
const { dispatch, action, intl } = this.props;
this.setState({ isLoading: true });
dispatch(action(intl)).then(() => {
this.setState({ isLoading: false });
}).catch((error) => {
this.setState({ isLoading: false });
});
}
render() {
const { intl, messages } = this.props;
return (
<>
<Form>
<Text size='xl' weight='bold'>{intl.formatMessage(messages.input_label)}</Text>
<Text theme='muted'>{intl.formatMessage(messages.input_hint)}</Text>
<FormActions>
<Button theme='primary' onClick={this.handleClick}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
</>
);
}
}

@ -0,0 +1,47 @@
import React from 'react';
import { useState } from 'react';
import { MessageDescriptor, useIntl } from 'react-intl';
import { Button, Form, FormActions, Text } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import { AppDispatch } from 'soapbox/store';
interface ICSVExporter {
messages: {
input_label: MessageDescriptor,
input_hint: MessageDescriptor,
submit: MessageDescriptor,
},
action: () => (dispatch: AppDispatch, getState: any) => Promise<void>,
}
const CSVExporter: React.FC<ICSVExporter> = ({ messages, action }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const [isLoading, setIsLoading] = useState(false);
const handleClick: React.MouseEventHandler = (event) => {
setIsLoading(true);
dispatch(action()).then(() => {
setIsLoading(false);
}).catch(() => {
setIsLoading(false);
});
};
return (
<Form>
<Text size='xl' weight='bold'>{intl.formatMessage(messages.input_label)}</Text>
<Text theme='muted'>{intl.formatMessage(messages.input_hint)}</Text>
<FormActions>
<Button theme='primary' onClick={handleClick} disabled={isLoading}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
);
};
export default CSVExporter;

@ -1,15 +1,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { defineMessages, useIntl } from 'react-intl';
import {
exportFollows,
exportBlocks,
exportMutes,
} from 'soapbox/actions/export_data';
import { getFeatures } from 'soapbox/utils/features';
import Column from '../ui/components/column';
@ -38,29 +34,16 @@ const muteMessages = defineMessages({
submit: { id: 'export_data.actions.export_mutes', defaultMessage: 'Export mutes' },
});
const mapStateToProps = state => ({
features: getFeatures(state.get('instance')),
});
export default @connect(mapStateToProps)
@injectIntl
class ExportData extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
features: PropTypes.object,
};
render() {
const { intl } = this.props;
const ExportData = () => {
const intl = useIntl();
return (
<Column icon='cloud-download-alt' label={intl.formatMessage(messages.heading)}>
<CSVExporter action={exportFollows} messages={followMessages} />
<CSVExporter action={exportBlocks} messages={blockMessages} />
<CSVExporter action={exportMutes} messages={muteMessages} />
</Column>
);
}
return (
<Column icon='cloud-download-alt' label={intl.formatMessage(messages.heading)}>
<CSVExporter action={exportFollows} messages={followMessages} />
<CSVExporter action={exportBlocks} messages={blockMessages} />
<CSVExporter action={exportMutes} messages={muteMessages} />
</Column>
);
};
}
export default ExportData;

@ -1,71 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Button, Form, FormActions, Text } from 'soapbox/components/ui';
export default @connect()
@injectIntl
class CSVImporter extends ImmutablePureComponent {
static propTypes = {
action: PropTypes.func.isRequired,
messages: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
file: null,
isLoading: false,
}
handleSubmit = (event) => {
const { dispatch, action, intl } = this.props;
const params = new FormData();
params.append('list', this.state.file);
this.setState({ isLoading: true });
dispatch(action(intl, params)).then(() => {
this.setState({ isLoading: false });
}).catch((error) => {
this.setState({ isLoading: false });
});
event.preventDefault();
}
handleFileChange = e => {
const [file] = e.target.files || [];
this.setState({ file });
}
render() {
const { intl, messages } = this.props;
return (
<Form onSubmit={this.handleSubmit} disabled={this.state.isLoading}>
<Text size='xl' weight='bold' tag='label'>{intl.formatMessage(messages.input_label)}</Text>
<div>
<input
className='text-black dark:text-white'
type='file'
accept={['.csv', 'text/csv']}
onChange={this.handleFileChange}
required
/>
<Text theme='muted'>{intl.formatMessage(messages.input_hint)}</Text>
</div>
<FormActions>
<Button type='submit' theme='primary' disabled={this.state.isLoading}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
);
}
}

@ -0,0 +1,66 @@
import React from 'react';
import { useState } from 'react';
import { MessageDescriptor, useIntl } from 'react-intl';
import { Button, FileInput, Form, FormActions, FormGroup, Text } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import { AppDispatch } from 'soapbox/store';
interface ICSVImporter {
messages: {
input_label: MessageDescriptor,
input_hint: MessageDescriptor,
submit: MessageDescriptor,
},
action: (params: FormData) => (dispatch: AppDispatch, getState: any) => Promise<void>,
}
const CSVImporter: React.FC<ICSVImporter> = ({ messages, action }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const [isLoading, setIsLoading] = useState(false);
const [file, setFile] = useState<File | null | undefined>(null);
const handleSubmit: React.FormEventHandler = (event) => {
const params = new FormData();
params.append('list', file!);
setIsLoading(true);
dispatch(action(params)).then(() => {
setIsLoading(false);
}).catch(() => {
setIsLoading(false);
});
event.preventDefault();
};
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = e => {
const file = e.target.files?.item(0);
setFile(file);
};
return (
<Form onSubmit={handleSubmit}>
<Text size='xl' weight='bold' tag='label'>{intl.formatMessage(messages.input_label)}</Text>
<FormGroup
hintText={<Text theme='muted'>{intl.formatMessage(messages.input_hint)}</Text>}
>
<FileInput
accept='.csv,text/csv'
onChange={handleFileChange}
required
/>
</FormGroup>
<FormActions>
<Button type='submit' theme='primary' disabled={isLoading}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
);
};
export default CSVImporter;

@ -1,14 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { defineMessages, useIntl } from 'react-intl';
import {
importFollows,
importBlocks,
importMutes,
} from 'soapbox/actions/import_data';
import { useAppSelector } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import Column from '../ui/components/column';
@ -38,29 +36,17 @@ const muteMessages = defineMessages({
submit: { id: 'import_data.actions.import_mutes', defaultMessage: 'Import mutes' },
});
const mapStateToProps = state => ({
features: getFeatures(state.get('instance')),
});
export default @connect(mapStateToProps)
@injectIntl
class ImportData extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
features: PropTypes.object,
};
render() {
const { intl, features } = this.props;
const ImportData = () => {
const intl = useIntl();
const features = getFeatures(useAppSelector((state) => state.instance));
return (
<Column icon='cloud-upload-alt' label={intl.formatMessage(messages.heading)}>
<CSVImporter action={importFollows} messages={followMessages} />
<CSVImporter action={importBlocks} messages={blockMessages} />
{features.importMutes && <CSVImporter action={importMutes} messages={muteMessages} />}
</Column>
);
}
return (
<Column icon='cloud-upload-alt' label={intl.formatMessage(messages.heading)}>
<CSVImporter action={importFollows} messages={followMessages} />
<CSVImporter action={importBlocks} messages={blockMessages} />
{features.importMutes && <CSVImporter action={importMutes} messages={muteMessages} />}
</Column>
);
};
}
export default ImportData;

@ -1,8 +1,10 @@
import { Map as ImmutableMap } from 'immutable';
import debounce from 'lodash/debounce';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppSelector } from 'soapbox/hooks';
@ -13,24 +15,42 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
const dispatch = useDispatch();
const suggestions = useAppSelector((state) => state.suggestions.get('items'));
const suggestionsToRender = suggestions.slice(0, 5);
const hasMore = useAppSelector((state) => !!state.suggestions.get('next'));
const isLoading = useAppSelector((state) => state.suggestions.get('isLoading'));
const handleLoadMore = debounce(() => {
if (isLoading) {
return null;
}
return dispatch(fetchSuggestions());
}, 300);
React.useEffect(() => {
dispatch(fetchSuggestions());
dispatch(fetchSuggestions({ limit: 20 }));
}, []);
const renderSuggestions = () => {
return (
<div className='sm:pt-4 sm:pb-10 flex flex-col divide-y divide-solid divide-gray-200 dark:divide-slate-700'>
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => (
<div key={suggestion.get('account')} className='py-2'>
<AccountContainer
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
id={suggestion.get('account')}
showProfileHoverCard={false}
/>
</div>
))}
<div className='sm:pt-4 sm:pb-10 flex flex-col'>
<ScrollableList
isLoading={isLoading}
scrollKey='suggestions'
onLoadMore={handleLoadMore}
hasMore={hasMore}
useWindowScroll={false}
style={{ height: 320 }}
>
{suggestions.map((suggestion: ImmutableMap<string, any>) => (
<div key={suggestion.get('account')} className='py-2'>
<AccountContainer
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
id={suggestion.get('account')}
showProfileHoverCard={false}
/>
</div>
))}
</ScrollableList>
</div>
);
};
@ -46,7 +66,7 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
};
const renderBody = () => {
if (suggestionsToRender.isEmpty()) {
if (suggestions.isEmpty()) {
return renderEmpty();
} else {
return renderSuggestions();

@ -2,7 +2,7 @@ import classNames from 'classnames';
import { History } from 'history';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage, IntlShape } from 'react-intl';
import { defineMessages, injectIntl, FormattedMessage, IntlShape, FormattedList } from 'react-intl';
import { withRouter } from 'react-router-dom';
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
@ -67,10 +67,9 @@ class QuotedStatus extends ImmutablePureComponent<IQuotedStatus> {
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
defaultMessage='Replying to {accounts}'
values={{
accounts: `@${account.username}`,
more: false,
}}
/>
</div>
@ -84,14 +83,21 @@ class QuotedStatus extends ImmutablePureComponent<IQuotedStatus> {
}
}
const accounts = to.slice(0, 2).map(account => <>@{account.username}</>).toArray();
if (to.size > 2) {
accounts.push(
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />,
);
}
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply'
defaultMessage='Replying to {accounts}{more}'
defaultMessage='Replying to {accounts}'
values={{
accounts: to.slice(0, 2).map(account => `@${account.username} `),
more: to.size > 2 && <FormattedMessage id='reply_mentions.more' defaultMessage='and {count} more' values={{ count: to.size - 2 }} />,
accounts: <FormattedList type='conjunction' value={accounts} />,
}}
/>
</div>

@ -561,7 +561,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
renderTombstone(id: string) {
return (
<div className='pb-4'>
<div className='py-4 pb-8'>
<Tombstone key={id} />
</div>
);

@ -3,7 +3,7 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import SiteLogo from 'soapbox/components/site-logo';
import { Button, Icon, Modal } from 'soapbox/components/ui';
import { Text, Button, Icon, Modal } from 'soapbox/components/ui';
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
const messages = defineMessages({
@ -17,6 +17,7 @@ interface ILandingPageModal {
onClose: (type: string) => void,
}
/** Login and links to display from the hamburger menu of the homepage. */
const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
const intl = useIntl();
@ -41,13 +42,13 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
<a
href={links.get('help')}
target='_blank'
className='p-3 flex items-center rounded-md dark:hover:bg-slate-900/50 hover:bg-gray-50'
className='p-3 space-x-3 flex items-center rounded-md dark:hover:bg-slate-900/50 hover:bg-gray-50'
>
<Icon src={require('@tabler/icons/icons/lifebuoy.svg')} className='flex-shrink-0 h-6 w-6 text-gray-400 dark:text-gray-200' />
<span className='ml-3 text-base font-medium text-gray-900 dark:text-gray-200'>
<Text weight='medium'>
{intl.formatMessage(messages.helpCenter)}
</span>
</Text>
</a>
</nav>
)}

@ -859,8 +859,6 @@
"reply_indicator.cancel": "إلغاء",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Encaboxar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Отказ",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "বাতিল করতে",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel·lar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Bloquejar {target}",
"report.block_hint": "També vols bloquejar aquest compte?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Annullà",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Zrušit",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Zablokovat {target}",
"report.block_hint": "Chcete zablokovat tento účet?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Canslo",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Annuller",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,8 @@
"reply_indicator.cancel": "Abbrechen",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "und {count, plural, one {einen weiteren Nutzer} other {# weitere Nutzer}}",
"reply_mentions.reply": "Antwort an {accounts}{more}",
"reply_mentions.more": "{count, plural, one {einen weiteren Nutzer} other {# weitere Nutzer}}",
"reply_mentions.reply": "Antwort an {accounts}",
"reply_mentions.reply_empty": "Antwort auf einen Beitrag",
"report.block": "{target} blockieren.",
"report.block_hint": "Soll dieses Konto zusammen mit der Meldung auch gleich blockiert werden?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Άκυρο",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,8 @@
"reply_indicator.cancel": "𐑒𐑨𐑯𐑕𐑩𐑤",
"reply_mentions.account.add": "𐑨𐑛 𐑑 𐑥𐑧𐑯𐑖𐑩𐑯𐑟",
"reply_mentions.account.remove": "𐑮𐑦𐑥𐑵𐑝 𐑓𐑮𐑪𐑥 𐑥𐑧𐑯𐑖𐑩𐑯𐑟",
"reply_mentions.more": "𐑯 {count} 𐑥𐑹",
"reply_mentions.reply": "𐑮𐑦𐑐𐑤𐑲𐑦𐑙 𐑑 {accounts}{more}",
"reply_mentions.more": "{count} 𐑥𐑹",
"reply_mentions.reply": "𐑮𐑦𐑐𐑤𐑲𐑦𐑙 𐑑 {accounts}",
"reply_mentions.reply_empty": "𐑮𐑦𐑐𐑤𐑲𐑦𐑙 𐑑 𐑐𐑴𐑕𐑑",
"report.block": "𐑚𐑤𐑪𐑒 {target}",
"report.block_hint": "𐑛𐑵 𐑿 𐑷𐑤𐑕𐑴 𐑢𐑪𐑯𐑑 𐑑 𐑚𐑤𐑪𐑒 𐑞𐑦𐑕 𐑩𐑒𐑬𐑯𐑑?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Nuligi",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancelar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancelar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Tühista",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Utzi",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "لغو",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Peruuta",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Annuler",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancelar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,8 @@
"reply_indicator.cancel": "ביטול",
"reply_mentions.account.add": "הוסף לאזכורים",
"reply_mentions.account.remove": "הסר מהאזכורים",
"reply_mentions.more": "ו-{count} עוד",
"reply_mentions.reply": "משיב ל-{accounts}{more}",
"reply_mentions.more": "{count} עוד",
"reply_mentions.reply": "משיב ל-{accounts}",
"reply_mentions.reply_empty": "משיב לפוסט",
"report.block": "חסום {target}",
"report.block_hint": "האם גם אתה רוצה לחסום את החשבון הזה?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Otkaži",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Mégsem",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Չեղարկել",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Batal",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Nihiligar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -788,8 +788,8 @@
"reply_indicator.cancel": "Hætta við",
"reply_mentions.account.add": "Bæta við í tilvísanirnar",
"reply_mentions.account.remove": "Fjarlægja úr tilvísunum",
"reply_mentions.more": "og {count} fleirum",
"reply_mentions.reply": "Að svara {accounts}{more}",
"reply_mentions.more": "{count} fleirum",
"reply_mentions.reply": "Að svara {accounts}",
"reply_mentions.reply_empty": "Að svara færslu",
"report.block": "Loka á {target}",
"report.block_hint": "Viltu líka loka á þennan reikning?",

@ -859,8 +859,8 @@
"reply_indicator.cancel": "Annulla",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "e ancora {count}",
"reply_mentions.reply": "Risponde a {accounts}{more}",
"reply_mentions.more": "ancora {count}",
"reply_mentions.reply": "Risponde a {accounts}",
"reply_mentions.reply_empty": "Rispondendo al contenuto",
"report.block": "Blocca {target}",
"report.block_hint": "Vuoi anche bloccare questa persona?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "キャンセル",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "{target}さんをブロック",
"report.block_hint": "このアカウントをブロックしますか?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "უარყოფა",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Қайтып алу",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "취소",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Annuleren",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancel",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Avbryt",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Anullar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -910,8 +910,8 @@
"reply_indicator.cancel": "Anuluj",
"reply_mentions.account.add": "Dodaj do wspomnianych",
"reply_mentions.account.remove": "Usuń z wspomnianych",
"reply_mentions.more": "i {count} więcej",
"reply_mentions.reply": "W odpowiedzi do {accounts}{more}",
"reply_mentions.more": "{count} więcej",
"reply_mentions.reply": "W odpowiedzi do {accounts}",
"reply_mentions.reply_empty": "W odpowiedzi na wpis",
"report.block": "Zablokuj {target}",
"report.block_hint": "Czy chcesz też zablokować to konto?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancelar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Cancelar",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Bloquear {target}",
"report.block_hint": "Desejas também bloquear esta conta?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Anulează",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Отмена",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Zrušiť",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Prekliči",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Anuloje",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Poništi",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Поништи",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Ångra",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "எதிராணை",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "రద్దు చెయ్యి",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "ยกเลิก",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "İptal",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "Відмінити",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "取消",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "屏蔽帐号 {target}",
"report.block_hint": "你是否也要屏蔽这个帐号呢?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "取消",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -859,8 +859,6 @@
"reply_indicator.cancel": "取消",
"reply_mentions.account.add": "Add to mentions",
"reply_mentions.account.remove": "Remove from mentions",
"reply_mentions.more": "and {count} more",
"reply_mentions.reply": "Replying to {accounts}{more}",
"reply_mentions.reply_empty": "Replying to post",
"report.block": "Block {target}",
"report.block_hint": "Do you also want to block this account?",

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { SUGGESTIONS_DISMISS } from 'soapbox/actions/suggestions';
@ -7,7 +7,8 @@ import reducer from '../suggestions';
describe('suggestions reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedSet(),
next: null,
isLoading: false,
}));
});

@ -19,6 +19,7 @@ import {
ACCOUNT_UNSUBSCRIBE_SUCCESS,
ACCOUNT_PIN_SUCCESS,
ACCOUNT_UNPIN_SUCCESS,
ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS,
RELATIONSHIPS_FETCH_SUCCESS,
} from '../actions/accounts';
import {
@ -108,6 +109,7 @@ export default function relationships(state = initialState, action) {
case ACCOUNT_PIN_SUCCESS:
case ACCOUNT_UNPIN_SUCCESS:
case ACCOUNT_NOTE_SUBMIT_SUCCESS:
case ACCOUNT_REMOVE_FROM_FOLLOWERS_SUCCESS:
return normalizeRelationship(state, action.relationship);
case RELATIONSHIPS_FETCH_SUCCESS:
return normalizeRelationships(state, action.relationships);

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks';
@ -14,7 +14,8 @@ import {
} from '../actions/suggestions';
const initialState = ImmutableMap({
items: ImmutableList(),
items: ImmutableOrderedSet(),
next: null,
isLoading: false,
});
@ -33,10 +34,11 @@ const importAccounts = (state, accounts) => {
});
};
const importSuggestions = (state, suggestions) => {
const importSuggestions = (state, suggestions, next) => {
return state.withMutations(state => {
state.set('items', fromJS(suggestions.map(x => ({ ...x, account: x.account.id }))));
state.update('items', items => items.concat(fromJS(suggestions.map(x => ({ ...x, account: x.account.id })))));
state.set('isLoading', false);
state.set('next', next);
});
};
@ -56,7 +58,7 @@ export default function suggestionsReducer(state = initialState, action) {
case SUGGESTIONS_FETCH_SUCCESS:
return importAccounts(state, action.accounts);
case SUGGESTIONS_V2_FETCH_SUCCESS:
return importSuggestions(state, action.suggestions);
return importSuggestions(state, action.suggestions, action.next);
case SUGGESTIONS_FETCH_FAIL:
case SUGGESTIONS_V2_FETCH_FAIL:
return state.set('isLoading', false);

@ -426,6 +426,15 @@ const getInstanceFeatures = (instance: Instance) => {
*/
remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
/**
* Ability to remove an account from your followers.
* @see POST /api/v1/accounts/:id/remove_from_followers
*/
removeFromFollowers: any([
v.software === MASTODON && gte(v.compatVersion, '3.5.0'),
v.software === PLEROMA && v.build === SOAPBOX && gte(v.version, '2.4.50'),
]),
reportMultipleStatuses: any([
v.software === MASTODON,
v.software === PLEROMA,

Loading…
Cancel
Save