mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 18:45:41 +01:00
Add ability to search for all accounts when creating a list in web UI (#33036)
This commit is contained in:
parent
6cf87762a4
commit
7135f513a4
25 changed files with 459 additions and 374 deletions
|
@ -1,66 +0,0 @@
|
||||||
import { defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
|
||||||
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
|
||||||
rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
|
|
||||||
rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ALERT_SHOW = 'ALERT_SHOW';
|
|
||||||
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
|
||||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
|
||||||
export const ALERT_NOOP = 'ALERT_NOOP';
|
|
||||||
|
|
||||||
export const dismissAlert = alert => ({
|
|
||||||
type: ALERT_DISMISS,
|
|
||||||
alert,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clearAlert = () => ({
|
|
||||||
type: ALERT_CLEAR,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const showAlert = alert => ({
|
|
||||||
type: ALERT_SHOW,
|
|
||||||
alert,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const showAlertForError = (error, skipNotFound = false) => {
|
|
||||||
if (error.response) {
|
|
||||||
const { data, status, statusText, headers } = error.response;
|
|
||||||
|
|
||||||
// Skip these errors as they are reflected in the UI
|
|
||||||
if (skipNotFound && (status === 404 || status === 410)) {
|
|
||||||
return { type: ALERT_NOOP };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rate limit errors
|
|
||||||
if (status === 429 && headers['x-ratelimit-reset']) {
|
|
||||||
return showAlert({
|
|
||||||
title: messages.rateLimitedTitle,
|
|
||||||
message: messages.rateLimitedMessage,
|
|
||||||
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return showAlert({
|
|
||||||
title: `${status}`,
|
|
||||||
message: data.error || statusText,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// An aborted request, e.g. due to reloading the browser window, it not really error
|
|
||||||
if (error.code === AxiosError.ECONNABORTED) {
|
|
||||||
return { type: ALERT_NOOP };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
return showAlert({
|
|
||||||
title: messages.unexpectedTitle,
|
|
||||||
message: messages.unexpectedMessage,
|
|
||||||
});
|
|
||||||
};
|
|
90
app/javascript/mastodon/actions/alerts.ts
Normal file
90
app/javascript/mastodon/actions/alerts.ts
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
|
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import type { AxiosResponse } from 'axios';
|
||||||
|
|
||||||
|
interface Alert {
|
||||||
|
title: string | MessageDescriptor;
|
||||||
|
message: string | MessageDescriptor;
|
||||||
|
values?: Record<string, string | number | Date>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiErrorResponse {
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
|
unexpectedMessage: {
|
||||||
|
id: 'alert.unexpected.message',
|
||||||
|
defaultMessage: 'An unexpected error occurred.',
|
||||||
|
},
|
||||||
|
rateLimitedTitle: {
|
||||||
|
id: 'alert.rate_limited.title',
|
||||||
|
defaultMessage: 'Rate limited',
|
||||||
|
},
|
||||||
|
rateLimitedMessage: {
|
||||||
|
id: 'alert.rate_limited.message',
|
||||||
|
defaultMessage: 'Please retry after {retry_time, time, medium}.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||||
|
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||||
|
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||||
|
export const ALERT_NOOP = 'ALERT_NOOP';
|
||||||
|
|
||||||
|
export const dismissAlert = (alert: Alert) => ({
|
||||||
|
type: ALERT_DISMISS,
|
||||||
|
alert,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const clearAlert = () => ({
|
||||||
|
type: ALERT_CLEAR,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const showAlert = (alert: Alert) => ({
|
||||||
|
type: ALERT_SHOW,
|
||||||
|
alert,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const showAlertForError = (error: unknown, skipNotFound = false) => {
|
||||||
|
if (error instanceof AxiosError && error.response) {
|
||||||
|
const { status, statusText, headers } = error.response;
|
||||||
|
const { data } = error.response as AxiosResponse<ApiErrorResponse>;
|
||||||
|
|
||||||
|
// Skip these errors as they are reflected in the UI
|
||||||
|
if (skipNotFound && (status === 404 || status === 410)) {
|
||||||
|
return { type: ALERT_NOOP };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit errors
|
||||||
|
if (status === 429 && headers['x-ratelimit-reset']) {
|
||||||
|
return showAlert({
|
||||||
|
title: messages.rateLimitedTitle,
|
||||||
|
message: messages.rateLimitedMessage,
|
||||||
|
values: {
|
||||||
|
retry_time: new Date(headers['x-ratelimit-reset'] as string),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return showAlert({
|
||||||
|
title: `${status}`,
|
||||||
|
message: data.error ?? statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// An aborted request, e.g. due to reloading the browser window, it not really error
|
||||||
|
if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) {
|
||||||
|
return { type: ALERT_NOOP };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return showAlert({
|
||||||
|
title: messages.unexpectedTitle,
|
||||||
|
message: messages.unexpectedMessage,
|
||||||
|
});
|
||||||
|
};
|
|
@ -5,3 +5,16 @@ export const apiSubmitAccountNote = (id: string, value: string) =>
|
||||||
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
|
||||||
comment: value,
|
comment: value,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiFollowAccount = (
|
||||||
|
id: string,
|
||||||
|
params?: {
|
||||||
|
reblogs: boolean;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/follow`, {
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiUnfollowAccount = (id: string) =>
|
||||||
|
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/unfollow`);
|
||||||
|
|
|
@ -1,175 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
|
||||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
|
||||||
import { FollowButton } from 'mastodon/components/follow_button';
|
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
|
||||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
|
||||||
|
|
||||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
|
||||||
import { me } from '../initial_state';
|
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
|
||||||
import { Button } from './button';
|
|
||||||
import { FollowersCounter } from './counters';
|
|
||||||
import { DisplayName } from './display_name';
|
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
|
||||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
|
||||||
mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
|
|
||||||
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
|
|
||||||
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
|
||||||
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
|
||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const Account = ({ size = 46, account, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const handleBlock = useCallback(() => {
|
|
||||||
onBlock(account);
|
|
||||||
}, [onBlock, account]);
|
|
||||||
|
|
||||||
const handleMute = useCallback(() => {
|
|
||||||
onMute(account);
|
|
||||||
}, [onMute, account]);
|
|
||||||
|
|
||||||
const handleMuteNotifications = useCallback(() => {
|
|
||||||
onMuteNotifications(account, true);
|
|
||||||
}, [onMuteNotifications, account]);
|
|
||||||
|
|
||||||
const handleUnmuteNotifications = useCallback(() => {
|
|
||||||
onMuteNotifications(account, false);
|
|
||||||
}, [onMuteNotifications, account]);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return <EmptyAccount size={size} minimal={minimal} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{account.get('display_name')}
|
|
||||||
{account.get('username')}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let buttons;
|
|
||||||
|
|
||||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
|
||||||
const muting = account.getIn(['relationship', 'muting']);
|
|
||||||
|
|
||||||
if (requested) {
|
|
||||||
buttons = <FollowButton accountId={account.get('id')} />;
|
|
||||||
} else if (blocking) {
|
|
||||||
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
|
|
||||||
} else if (muting) {
|
|
||||||
let menu;
|
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
|
||||||
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
|
|
||||||
} else {
|
|
||||||
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons = (
|
|
||||||
<>
|
|
||||||
<DropdownMenuContainer
|
|
||||||
items={menu}
|
|
||||||
icon='ellipsis-h'
|
|
||||||
iconComponent={MoreHorizIcon}
|
|
||||||
direction='right'
|
|
||||||
title={intl.formatMessage(messages.more)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (defaultAction === 'mute') {
|
|
||||||
buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
|
|
||||||
} else if (defaultAction === 'block') {
|
|
||||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
|
|
||||||
} else {
|
|
||||||
buttons = <FollowButton accountId={account.get('id')} />;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttons = <FollowButton accountId={account.get('id')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
let muteTimeRemaining;
|
|
||||||
|
|
||||||
if (account.get('mute_expires_at')) {
|
|
||||||
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let verification;
|
|
||||||
|
|
||||||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
|
||||||
|
|
||||||
if (firstVerifiedField) {
|
|
||||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
|
||||||
<div className='account__wrapper'>
|
|
||||||
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`} data-hover-card-account={account.get('id')}>
|
|
||||||
<div className='account__avatar-wrapper'>
|
|
||||||
<Avatar account={account} size={size} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__contents'>
|
|
||||||
<DisplayName account={account} />
|
|
||||||
{!minimal && (
|
|
||||||
<div className='account__details'>
|
|
||||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{!minimal && (
|
|
||||||
<div className='account__relationship'>
|
|
||||||
{buttons}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{withBio && (account.get('note').length > 0 ? (
|
|
||||||
<div
|
|
||||||
className='account__note translate'
|
|
||||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Account.propTypes = {
|
|
||||||
size: PropTypes.number,
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
onBlock: PropTypes.func,
|
|
||||||
onMute: PropTypes.func,
|
|
||||||
onMuteNotifications: PropTypes.func,
|
|
||||||
hidden: PropTypes.bool,
|
|
||||||
minimal: PropTypes.bool,
|
|
||||||
defaultAction: PropTypes.string,
|
|
||||||
withBio: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Account;
|
|
235
app/javascript/mastodon/components/account.tsx
Normal file
235
app/javascript/mastodon/components/account.tsx
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import {
|
||||||
|
blockAccount,
|
||||||
|
unblockAccount,
|
||||||
|
muteAccount,
|
||||||
|
unmuteAccount,
|
||||||
|
} from 'mastodon/actions/accounts';
|
||||||
|
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||||
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { FollowersCounter } from 'mastodon/components/counters';
|
||||||
|
import { DisplayName } from 'mastodon/components/display_name';
|
||||||
|
import { FollowButton } from 'mastodon/components/follow_button';
|
||||||
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||||
|
import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
cancel_follow_request: {
|
||||||
|
id: 'account.cancel_follow_request',
|
||||||
|
defaultMessage: 'Withdraw follow request',
|
||||||
|
},
|
||||||
|
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||||
|
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||||
|
mute_notifications: {
|
||||||
|
id: 'account.mute_notifications_short',
|
||||||
|
defaultMessage: 'Mute notifications',
|
||||||
|
},
|
||||||
|
unmute_notifications: {
|
||||||
|
id: 'account.unmute_notifications_short',
|
||||||
|
defaultMessage: 'Unmute notifications',
|
||||||
|
},
|
||||||
|
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
||||||
|
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
||||||
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Account: React.FC<{
|
||||||
|
size?: number;
|
||||||
|
id: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
minimal?: boolean;
|
||||||
|
defaultAction?: 'block' | 'mute';
|
||||||
|
withBio?: boolean;
|
||||||
|
}> = ({ id, size = 46, hidden, minimal, defaultAction, withBio }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(id));
|
||||||
|
const relationship = useAppSelector((state) => state.relationships.get(id));
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleBlock = useCallback(() => {
|
||||||
|
if (relationship?.blocking) {
|
||||||
|
dispatch(unblockAccount(id));
|
||||||
|
} else {
|
||||||
|
dispatch(blockAccount(id));
|
||||||
|
}
|
||||||
|
}, [dispatch, id, relationship]);
|
||||||
|
|
||||||
|
const handleMute = useCallback(() => {
|
||||||
|
if (relationship?.muting) {
|
||||||
|
dispatch(unmuteAccount(id));
|
||||||
|
} else {
|
||||||
|
dispatch(initMuteModal(account));
|
||||||
|
}
|
||||||
|
}, [dispatch, id, account, relationship]);
|
||||||
|
|
||||||
|
const handleMuteNotifications = useCallback(() => {
|
||||||
|
dispatch(muteAccount(id, true));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const handleUnmuteNotifications = useCallback(() => {
|
||||||
|
dispatch(muteAccount(id, false));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{account?.display_name}
|
||||||
|
{account?.username}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttons;
|
||||||
|
|
||||||
|
if (account && account.id !== me && relationship) {
|
||||||
|
const { requested, blocking, muting } = relationship;
|
||||||
|
|
||||||
|
if (requested) {
|
||||||
|
buttons = <FollowButton accountId={id} />;
|
||||||
|
} else if (blocking) {
|
||||||
|
buttons = (
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(messages.unblock)}
|
||||||
|
onClick={handleBlock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (muting) {
|
||||||
|
const menu = [
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(
|
||||||
|
relationship.muting_notifications
|
||||||
|
? messages.unmute_notifications
|
||||||
|
: messages.mute_notifications,
|
||||||
|
),
|
||||||
|
action: relationship.muting_notifications
|
||||||
|
? handleUnmuteNotifications
|
||||||
|
: handleMuteNotifications,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
buttons = (
|
||||||
|
<>
|
||||||
|
<DropdownMenu
|
||||||
|
items={menu}
|
||||||
|
icon='ellipsis-h'
|
||||||
|
iconComponent={MoreHorizIcon}
|
||||||
|
direction='right'
|
||||||
|
title={intl.formatMessage(messages.more)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(messages.unmute)}
|
||||||
|
onClick={handleMute}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (defaultAction === 'mute') {
|
||||||
|
buttons = (
|
||||||
|
<Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
|
||||||
|
);
|
||||||
|
} else if (defaultAction === 'block') {
|
||||||
|
buttons = (
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(messages.block)}
|
||||||
|
onClick={handleBlock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buttons = <FollowButton accountId={id} />;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttons = <FollowButton accountId={id} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let muteTimeRemaining;
|
||||||
|
|
||||||
|
if (account?.mute_expires_at) {
|
||||||
|
muteTimeRemaining = (
|
||||||
|
<>
|
||||||
|
· <RelativeTimestamp timestamp={account.mute_expires_at} futureDate />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let verification;
|
||||||
|
|
||||||
|
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
|
||||||
|
|
||||||
|
if (firstVerifiedField) {
|
||||||
|
verification = <VerifiedBadge link={firstVerifiedField.value} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<Link
|
||||||
|
className='account__display-name'
|
||||||
|
title={account?.acct}
|
||||||
|
to={`/@${account?.acct}`}
|
||||||
|
data-hover-card-account={id}
|
||||||
|
>
|
||||||
|
<div className='account__avatar-wrapper'>
|
||||||
|
{account ? (
|
||||||
|
<Avatar account={account} size={size} />
|
||||||
|
) : (
|
||||||
|
<Skeleton width={size} height={size} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='account__contents'>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
|
||||||
|
{!minimal && (
|
||||||
|
<div className='account__details'>
|
||||||
|
{account ? (
|
||||||
|
<>
|
||||||
|
<ShortNumber
|
||||||
|
value={account.followers_count}
|
||||||
|
renderer={FollowersCounter}
|
||||||
|
/>{' '}
|
||||||
|
{verification} {muteTimeRemaining}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Skeleton width='7ch' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{!minimal && <div className='account__relationship'>{buttons}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{account &&
|
||||||
|
withBio &&
|
||||||
|
(account.note.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className='account__note translate'
|
||||||
|
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='account__note account__note--missing'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.no_bio'
|
||||||
|
defaultMessage='No description provided.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,33 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { DisplayName } from 'mastodon/components/display_name';
|
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
size?: number;
|
|
||||||
minimal?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmptyAccount: React.FC<Props> = ({
|
|
||||||
size = 46,
|
|
||||||
minimal = false,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
|
||||||
<div className='account__wrapper'>
|
|
||||||
<div className='account__display-name'>
|
|
||||||
<div className='account__avatar-wrapper'>
|
|
||||||
<Skeleton width={size} height={size} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<DisplayName />
|
|
||||||
<Skeleton width='7ch' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -8,10 +8,10 @@ import { Link } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { fetchServer } from 'mastodon/actions/server';
|
import { fetchServer } from 'mastodon/actions/server';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
import Account from 'mastodon/containers/account_container';
|
|
||||||
import { domain } from 'mastodon/initial_state';
|
import { domain } from 'mastodon/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
|
|
||||||
import {
|
|
||||||
followAccount,
|
|
||||||
blockAccount,
|
|
||||||
unblockAccount,
|
|
||||||
muteAccount,
|
|
||||||
unmuteAccount,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import { initMuteModal } from '../actions/mutes';
|
|
||||||
import Account from '../components/account';
|
|
||||||
import { makeGetAccount } from '../selectors';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
account: getAccount(state, props.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
|
||||||
onFollow (account) {
|
|
||||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
|
|
||||||
} else {
|
|
||||||
dispatch(followAccount(account.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlock (account) {
|
|
||||||
if (account.getIn(['relationship', 'blocking'])) {
|
|
||||||
dispatch(unblockAccount(account.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(blockAccount(account.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onMute (account) {
|
|
||||||
if (account.getIn(['relationship', 'muting'])) {
|
|
||||||
dispatch(unmuteAccount(account.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(initMuteModal(account));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
onMuteNotifications (account, notifications) {
|
|
||||||
dispatch(muteAccount(account.get('id'), notifications));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
|
|
|
@ -13,11 +13,11 @@ import { connect } from 'react-redux';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
||||||
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
|
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
import Account from 'mastodon/containers/account_container';
|
|
||||||
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
@ -9,11 +9,11 @@ import { connect } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
|
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
|
|
||||||
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
|
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -70,7 +70,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} defaultAction='block' />,
|
<Account key={id} id={id} defaultAction='block' />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||||
import Account from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
@ -20,7 +20,6 @@ const messages = defineMessages({
|
||||||
export const NavigationBar = () => {
|
export const NavigationBar = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const account = useSelector(state => state.getIn(['accounts', me]));
|
|
||||||
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
||||||
|
|
||||||
const handleCancelClick = useCallback(() => {
|
const handleCancelClick = useCallback(() => {
|
||||||
|
@ -29,7 +28,7 @@ export const NavigationBar = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='navigation-bar'>
|
<div className='navigation-bar'>
|
||||||
<Account account={account} minimal />
|
<Account id={me} minimal />
|
||||||
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
import { expandSearch } from 'mastodon/actions/search';
|
import { expandSearch } from 'mastodon/actions/search';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { LoadMore } from 'mastodon/components/load_more';
|
import { LoadMore } from 'mastodon/components/load_more';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
@ -13,7 +14,6 @@ import { SearchSection } from 'mastodon/features/explore/components/search_secti
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
||||||
import AccountContainer from '../../../containers/account_container';
|
|
||||||
import StatusContainer from '../../../containers/status_container';
|
import StatusContainer from '../../../containers/status_container';
|
||||||
|
|
||||||
const INITIAL_PAGE_LIMIT = 10;
|
const INITIAL_PAGE_LIMIT = 10;
|
||||||
|
@ -49,7 +49,7 @@ export const SearchResults = () => {
|
||||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||||
accounts = (
|
accounts = (
|
||||||
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
<SearchSection title={<><Icon id='users' icon={PeopleIcon} /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
||||||
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
{withoutLastResult(results.get('accounts')).map(accountId => <Account key={accountId} id={accountId} />)}
|
||||||
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
|
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={handleLoadMoreAccounts} />}
|
||||||
</SearchSection>
|
</SearchSection>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,10 +13,10 @@ import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
import { submitSearch, expandSearch } from 'mastodon/actions/search';
|
import { submitSearch, expandSearch } from 'mastodon/actions/search';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import Account from 'mastodon/containers/account_container';
|
|
||||||
import Status from 'mastodon/containers/status_container';
|
import Status from 'mastodon/containers/status_container';
|
||||||
|
|
||||||
import { SearchSection } from './components/search_section';
|
import { SearchSection } from './components/search_section';
|
||||||
|
|
|
@ -12,11 +12,11 @@ import { debounce } from 'lodash';
|
||||||
|
|
||||||
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
||||||
import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
|
import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import AccountContainer from 'mastodon/containers/account_container';
|
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -87,7 +87,7 @@ class Favourites extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<Account key={id} id={id} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
|
@ -23,7 +24,6 @@ import {
|
||||||
import { ColumnBackButton } from '../../components/column_back_button';
|
import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
|
||||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
@ -175,7 +175,7 @@ class Followers extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{forceEmptyState ? [] : accountIds.map(id =>
|
{forceEmptyState ? [] : accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<Account key={id} id={id} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
|
@ -23,7 +24,6 @@ import {
|
||||||
import { ColumnBackButton } from '../../components/column_back_button';
|
import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
|
||||||
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
@ -175,7 +175,7 @@ class Following extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{forceEmptyState ? [] : accountIds.map(id =>
|
{forceEmptyState ? [] : accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<Account key={id} id={id} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -9,9 +9,13 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||||
|
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||||
|
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||||
import { fetchList } from 'mastodon/actions/lists';
|
import { fetchList } from 'mastodon/actions/lists';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { apiRequest } from 'mastodon/api';
|
import { apiRequest } from 'mastodon/api';
|
||||||
|
import { apiFollowAccount } from 'mastodon/api/accounts';
|
||||||
import {
|
import {
|
||||||
apiGetAccounts,
|
apiGetAccounts,
|
||||||
apiAddAccountToList,
|
apiAddAccountToList,
|
||||||
|
@ -28,13 +32,14 @@ import { DisplayName } from 'mastodon/components/display_name';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
|
heading: { id: 'column.list_members', defaultMessage: 'Manage list members' },
|
||||||
placeholder: {
|
placeholder: {
|
||||||
id: 'lists.search_placeholder',
|
id: 'lists.search',
|
||||||
defaultMessage: 'Search people you follow',
|
defaultMessage: 'Search',
|
||||||
},
|
},
|
||||||
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
|
enterSearch: { id: 'lists.add_to_list', defaultMessage: 'Add to list' },
|
||||||
add: { id: 'lists.add_member', defaultMessage: 'Add' },
|
add: { id: 'lists.add_member', defaultMessage: 'Add' },
|
||||||
|
@ -51,17 +56,51 @@ const AccountItem: React.FC<{
|
||||||
onToggle: (accountId: string) => void;
|
onToggle: (accountId: string) => void;
|
||||||
}> = ({ accountId, listId, partOfList, onToggle }) => {
|
}> = ({ accountId, listId, partOfList, onToggle }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
const relationship = useAppSelector((state) =>
|
||||||
|
accountId ? state.relationships.get(accountId) : undefined,
|
||||||
|
);
|
||||||
|
const following =
|
||||||
|
accountId === me || relationship?.following || relationship?.requested;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId) {
|
||||||
|
dispatch(fetchRelationships([accountId]));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (partOfList) {
|
if (partOfList) {
|
||||||
void apiRemoveAccountFromList(listId, accountId);
|
void apiRemoveAccountFromList(listId, accountId);
|
||||||
|
onToggle(accountId);
|
||||||
} else {
|
} else {
|
||||||
void apiAddAccountToList(listId, accountId);
|
if (following) {
|
||||||
|
void apiAddAccountToList(listId, accountId);
|
||||||
|
onToggle(accountId);
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM_FOLLOW_TO_LIST',
|
||||||
|
modalProps: {
|
||||||
|
accountId,
|
||||||
|
onConfirm: () => {
|
||||||
|
apiFollowAccount(accountId)
|
||||||
|
.then(() => apiAddAccountToList(listId, accountId))
|
||||||
|
.then(() => {
|
||||||
|
onToggle(accountId);
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
dispatch(showAlertForError(err));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}, [dispatch, accountId, following, listId, partOfList, onToggle]);
|
||||||
onToggle(accountId);
|
|
||||||
}, [accountId, listId, partOfList, onToggle]);
|
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -186,8 +225,7 @@ const ListMembers: React.FC<{
|
||||||
signal: searchRequestRef.current.signal,
|
signal: searchRequestRef.current.signal,
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: false,
|
resolve: true,
|
||||||
following: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|
|
@ -11,11 +11,11 @@ import { connect } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
|
|
||||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -72,7 +72,7 @@ class Mutes extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} defaultAction='mute' />,
|
<Account key={id} id={id} defaultAction='mute' />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,8 @@ import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
|
||||||
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import AccountContainer from 'mastodon/containers/account_container';
|
|
||||||
import StatusContainer from 'mastodon/containers/status_container';
|
import StatusContainer from 'mastodon/containers/status_container';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
@ -147,7 +147,7 @@ class Notification extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
|
<Account id={account.get('id')} hidden={this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -167,7 +167,7 @@ class Notification extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
|
<FollowRequestContainer id={account.get('id')} hidden={this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
@ -420,7 +420,7 @@ class Notification extends ImmutablePureComponent {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountContainer id={account.get('id')} hidden={this.props.hidden} />
|
<Account id={account.get('id')} hidden={this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,11 +14,11 @@ import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||||
import { markAsPartial } from 'mastodon/actions/timelines';
|
import { markAsPartial } from 'mastodon/actions/timelines';
|
||||||
import { apiRequest } from 'mastodon/api';
|
import { apiRequest } from 'mastodon/api';
|
||||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
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';
|
||||||
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
|
import { ColumnSearchHeader } from 'mastodon/components/column_search_header';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import Account from 'mastodon/containers/account_container';
|
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -170,12 +170,7 @@ export const Follows: React.FC<{
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{displayedAccountIds.map((accountId) => (
|
{displayedAccountIds.map((accountId) => (
|
||||||
<Account
|
<Account id={accountId} key={accountId} withBio />
|
||||||
/* @ts-expect-error inferred props are wrong */
|
|
||||||
id={accountId}
|
|
||||||
key={accountId}
|
|
||||||
withBio
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@ import { connect } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
||||||
|
import { Account } from 'mastodon/components/account';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
|
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import AccountContainer from '../../containers/account_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -88,7 +88,7 @@ class Reblogs extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{accountIds.map(id =>
|
{accountIds.map(id =>
|
||||||
<AccountContainer key={id} id={id} withNote={false} />,
|
<Account key={id} id={id} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import type { BaseConfirmationModalProps } from './confirmation_modal';
|
||||||
|
import { ConfirmationModal } from './confirmation_modal';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: 'confirmations.follow_to_list.title',
|
||||||
|
defaultMessage: 'Follow user?',
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
id: 'confirmations.follow_to_list.confirm',
|
||||||
|
defaultMessage: 'Follow and add to list',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ConfirmFollowToListModal: React.FC<
|
||||||
|
{
|
||||||
|
accountId: string;
|
||||||
|
onConfirm: () => void;
|
||||||
|
} & BaseConfirmationModalProps
|
||||||
|
> = ({ accountId, onConfirm, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationModal
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
message={
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.follow_to_list.message'
|
||||||
|
defaultMessage='You need to be following {name} to add them to a list.'
|
||||||
|
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
confirm={intl.formatMessage(messages.confirm)}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,3 +6,4 @@ export { ConfirmEditStatusModal } from './edit_status';
|
||||||
export { ConfirmUnfollowModal } from './unfollow';
|
export { ConfirmUnfollowModal } from './unfollow';
|
||||||
export { ConfirmClearNotificationsModal } from './clear_notifications';
|
export { ConfirmClearNotificationsModal } from './clear_notifications';
|
||||||
export { ConfirmLogOutModal } from './log_out';
|
export { ConfirmLogOutModal } from './log_out';
|
||||||
|
export { ConfirmFollowToListModal } from './follow_to_list';
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
ConfirmUnfollowModal,
|
ConfirmUnfollowModal,
|
||||||
ConfirmClearNotificationsModal,
|
ConfirmClearNotificationsModal,
|
||||||
ConfirmLogOutModal,
|
ConfirmLogOutModal,
|
||||||
|
ConfirmFollowToListModal,
|
||||||
} from './confirmation_modals';
|
} from './confirmation_modals';
|
||||||
import FocalPointModal from './focal_point_modal';
|
import FocalPointModal from './focal_point_modal';
|
||||||
import ImageModal from './image_modal';
|
import ImageModal from './image_modal';
|
||||||
|
@ -56,6 +57,7 @@ export const MODAL_COMPONENTS = {
|
||||||
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
|
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
|
||||||
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
|
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
|
||||||
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
||||||
|
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
|
||||||
'MUTE': MuteModal,
|
'MUTE': MuteModal,
|
||||||
'BLOCK': BlockModal,
|
'BLOCK': BlockModal,
|
||||||
'DOMAIN_BLOCK': DomainBlockModal,
|
'DOMAIN_BLOCK': DomainBlockModal,
|
||||||
|
|
|
@ -205,6 +205,9 @@
|
||||||
"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.edit.title": "Overwrite post?",
|
"confirmations.edit.title": "Overwrite post?",
|
||||||
|
"confirmations.follow_to_list.confirm": "Follow and add to list",
|
||||||
|
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
|
||||||
|
"confirmations.follow_to_list.title": "Follow user?",
|
||||||
"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.logout.title": "Log out?",
|
"confirmations.logout.title": "Log out?",
|
||||||
|
@ -493,7 +496,7 @@
|
||||||
"lists.replies_policy.list": "Members of the list",
|
"lists.replies_policy.list": "Members of the list",
|
||||||
"lists.replies_policy.none": "No one",
|
"lists.replies_policy.none": "No one",
|
||||||
"lists.save": "Save",
|
"lists.save": "Save",
|
||||||
"lists.search_placeholder": "Search people you follow",
|
"lists.search": "Search",
|
||||||
"lists.show_replies_to": "Include replies from list members to",
|
"lists.show_replies_to": "Include replies from list members to",
|
||||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||||
"loading_indicator.label": "Loading…",
|
"loading_indicator.label": "Loading…",
|
||||||
|
|
Loading…
Reference in a new issue