Merge branch 'ts' into 'develop'

TypeScript, FC, styles and fixes

See merge request soapbox-pub/soapbox-fe!1467
environments/review-develop-3zknud/deployments/115
marcin mikołajczak 2 years ago
commit a005c7d2d9

@ -10,7 +10,7 @@ export function pinHost(host) {
const state = getState();
const pinnedHosts = getPinnedHosts(state);
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.add(host)));
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.push(host)));
};
}
@ -19,6 +19,6 @@ export function unpinHost(host) {
const state = getState();
const pinnedHosts = getPinnedHosts(state);
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.delete(host)));
return dispatch(changeSetting(['remote_timeline', 'pinnedHosts'], pinnedHosts.filter((value) => value !== host)));
};
}

@ -1,24 +0,0 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StillImage from 'soapbox/components/still_image';
export default class AvatarOverlay extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.record.isRequired,
friend: ImmutablePropTypes.map.isRequired,
};
render() {
const { account, friend } = this.props;
return (
<div className='account__avatar-overlay'>
<StillImage src={account.get('avatar')} className='account__avatar-overlay-base' />
<StillImage src={friend.get('avatar')} className='account__avatar-overlay-overlay' />
</div>
);
}
}

@ -0,0 +1,19 @@
import React from 'react';
import StillImage from 'soapbox/components/still_image';
import type { Account as AccountEntity } from 'soapbox/types/entities';
interface IAvatarOverlay {
account: AccountEntity,
friend: AccountEntity,
}
const AvatarOverlay: React.FC<IAvatarOverlay> = ({ account, friend }) => (
<div className='account__avatar-overlay'>
<StillImage src={account.avatar} className='account__avatar-overlay-base' />
<StillImage src={friend.avatar} className='account__avatar-overlay-overlay' />
</div>
);
export default AvatarOverlay;

@ -12,6 +12,7 @@ export default class IconButton extends React.PureComponent {
static propTypes = {
className: PropTypes.string,
iconClassName: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string,
src: PropTypes.string,
@ -99,6 +100,7 @@ export default class IconButton extends React.PureComponent {
active,
animate,
className,
iconClassName,
disabled,
expanded,
icon,
@ -144,7 +146,7 @@ export default class IconButton extends React.PureComponent {
<div style={src ? {} : style}>
{emoji
? <div className='icon-button__emoji' dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
: <Icon id={icon} src={src} fixedWidth aria-hidden='true' />}
: <Icon className={iconClassName} id={icon} src={src} fixedWidth aria-hidden='true' />}
</div>
{text && <span className='icon-button__text'>{text}</span>}
</button>
@ -174,7 +176,7 @@ export default class IconButton extends React.PureComponent {
<div style={src ? {} : style}>
{emoji
? <div className='icon-button__emoji' style={{ transform: `rotate(${rotate}deg)` }} dangerouslySetInnerHTML={{ __html: emojify(emoji) }} aria-hidden='true' />
: <Icon id={icon} src={src} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />}
: <Icon className={iconClassName} id={icon} src={src} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />}
</div>
{text && <span className='icon-button__text'>{text}</span>}
</button>

@ -1,35 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'soapbox/components/icon';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
export default @injectIntl
class LoadGap extends React.PureComponent {
static propTypes = {
disabled: PropTypes.bool,
maxId: PropTypes.string,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onClick(this.props.maxId);
}
render() {
const { disabled, intl } = this.props;
return (
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<Icon id='ellipsis-h' />
</button>
);
}
}

@ -0,0 +1,28 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
interface ILoadGap {
disabled?: boolean,
maxId: string,
onClick: (id: string) => void,
}
const LoadGap: React.FC<ILoadGap> = ({ disabled, maxId, onClick }) => {
const intl = useIntl();
const handleClick = () => onClick(maxId);
return (
<button className='load-more load-gap' disabled={disabled} onClick={handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<Icon id='ellipsis-h' />
</button>
);
};
export default LoadGap;

@ -4,7 +4,9 @@ import PTRComponent from 'react-simple-pull-to-refresh';
import { Spinner } from 'soapbox/components/ui';
interface IPullToRefresh {
onRefresh?: () => Promise<any>
onRefresh?: () => Promise<any>;
refreshingContent?: JSX.Element | string;
pullingContent?: JSX.Element | string;
}
/**

@ -1,30 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import PullToRefresh from './pull-to-refresh';
/**
* Pullable:
* Basic "pull to refresh" without the refresh.
* Just visual feedback.
*/
export default class Pullable extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,
}
render() {
const { children } = this.props;
return (
<PullToRefresh
pullingContent={null}
refreshingContent={null}
>
{children}
</PullToRefresh>
);
}
}

@ -0,0 +1,24 @@
import React from 'react';
import PullToRefresh from './pull-to-refresh';
interface IPullable {
children: JSX.Element,
}
/**
* Pullable:
* Basic "pull to refresh" without the refresh.
* Just visual feedback.
*/
const Pullable: React.FC<IPullable> = ({ children }) =>(
<PullToRefresh
pullingContent={undefined}
// @ts-ignore
refreshingContent={null}
>
{children}
</PullToRefresh>
);
export default Pullable;

@ -1,35 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
export default class RadioButton extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
checked: PropTypes.bool,
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
label: PropTypes.node.isRequired,
};
render() {
const { name, value, checked, onChange, label } = this.props;
return (
<label className='radio-button'>
<input
name={name}
type='radio'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('radio-button__input', { checked })} />
<span>{label}</span>
</label>
);
}
}

@ -0,0 +1,28 @@
import classNames from 'classnames';
import React from 'react';
interface IRadioButton {
value: string,
checked?: boolean,
name: string,
onChange: React.ChangeEventHandler<HTMLInputElement>,
label: JSX.Element,
}
const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, label }) => (
<label className='radio-button'>
<input
name={name}
type='radio'
value={value}
checked={checked}
onChange={onChange}
/>
<span className={classNames('radio-button__input', { checked })} />
<span>{label}</span>
</label>
);
export default RadioButton;

@ -6,16 +6,20 @@ import { useSoapboxConfig, useSettings, useSystemTheme } from 'soapbox/hooks';
interface ISiteLogo extends React.ComponentProps<'img'> {
/** Extra class names for the <img> element. */
className?: string,
/** Override theme setting for <SitePreview /> */
theme?: 'dark' | 'light',
}
/** Display the most appropriate site logo based on the theme and configuration. */
const SiteLogo: React.FC<ISiteLogo> = ({ className, ...rest }) => {
const SiteLogo: React.FC<ISiteLogo> = ({ className, theme, ...rest }) => {
const { logo, logoDarkMode } = useSoapboxConfig();
const settings = useSettings();
const systemTheme = useSystemTheme();
const userTheme = settings.get('themeMode');
const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
const darkMode = theme
? theme === 'dark'
: (userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark'));
/** Soapbox logo. */
const soapboxLogo = darkMode

@ -0,0 +1,75 @@
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { FormattedList, FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
import { useAppDispatch } from 'soapbox/hooks';
import type { Status } from 'soapbox/types/entities';
interface IStatusReplyMentions {
status: Status,
}
const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status }) => {
const dispatch = useAppDispatch();
const handleOpenMentionsModal: React.MouseEventHandler<HTMLSpanElement> = (e) => {
e.stopPropagation();
dispatch(openModal('MENTIONS', {
username: status.getIn(['account', 'acct']),
statusId: status.get('id'),
}));
};
if (!status.get('in_reply_to_id')) {
return null;
}
const to = status.get('mentions', ImmutableList());
// The post is a reply, but it has no mentions.
// Rare, but it can happen.
if (to.size === 0) {
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply_empty'
defaultMessage='Replying to post'
/>
</div>
);
}
// 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={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}'
values={{
accounts: <FormattedList type='conjunction' value={accounts} />,
}}
/>
</div>
);
};
export default StatusReplyMentions;

@ -6,18 +6,17 @@ import { injectIntl, FormattedMessage, IntlShape, defineMessages } from 'react-i
import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom';
import Icon from 'soapbox/components/icon';
import AccountContainer from 'soapbox/containers/account_container';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
import Card from 'soapbox/features/status/components/card';
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
import AccountContainer from '../containers/account_container';
import Card from '../features/status/components/card';
import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import Bundle from 'soapbox/features/ui/components/bundle';
import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components';
import AttachmentThumbs from './attachment-thumbs';
import StatusReplyMentions from './status-reply-mentions';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
import StatusReplyMentions from './status_reply_mentions';
import { HStack, Text } from './ui';
import type { History } from 'history';

@ -1,89 +0,0 @@
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 { FormattedList, FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
const mapDispatchToProps = (dispatch) => ({
onOpenMentionsModal(username, statusId) {
dispatch(openModal('MENTIONS', {
username,
statusId,
}));
},
});
export default @connect(null, mapDispatchToProps)
@injectIntl
class StatusReplyMentions extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.record.isRequired,
onOpenMentionsModal: PropTypes.func,
}
handleOpenMentionsModal = (e) => {
const { status, onOpenMentionsModal } = this.props;
e.stopPropagation();
onOpenMentionsModal(status.getIn(['account', 'acct']), status.get('id'));
}
render() {
const { status } = this.props;
if (!status.get('in_reply_to_id')) {
return null;
}
const to = status.get('mentions', ImmutableList());
// The post is a reply, but it has no mentions.
// Rare, but it can happen.
if (to.size === 0) {
return (
<div className='reply-mentions'>
<FormattedMessage
id='reply_mentions.reply_empty'
defaultMessage='Replying to post'
/>
</div>
);
}
// 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}'
values={{
accounts: <FormattedList type='conjunction' value={accounts} />,
}}
/>
</div>
);
}
}

@ -28,6 +28,7 @@ interface IWidget {
actionIcon?: string,
/** Text for the action. */
actionTitle?: string,
action?: JSX.Element,
}
/** Sidebar widget. */
@ -37,19 +38,20 @@ const Widget: React.FC<IWidget> = ({
onActionClick,
actionIcon = require('@tabler/icons/icons/arrow-right.svg'),
actionTitle,
action,
}): JSX.Element => {
return (
<Stack space={2}>
<HStack alignItems='center'>
<WidgetTitle title={title} />
{onActionClick && (
{action || (onActionClick && (
<IconButton
className='w-6 h-6 ml-2 text-black dark:text-white'
src={actionIcon}
onClick={onActionClick}
title={actionTitle}
/>
)}
))}
</HStack>
<WidgetBody>{children}</WidgetBody>
</Stack>

@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
const messages = defineMessages({
otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' },
otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' },
otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' },
});
export default @connect()
@injectIntl
class OtpAuthForm extends ImmutablePureComponent {
state = {
isLoading: false,
code_error: '',
shouldRedirect: false,
}
static propTypes = {
intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
mfa_token: PropTypes.string,
};
getFormData = (form) => {
return Object.fromEntries(
Array.from(form).map(i => [i.name, i.value]),
);
}
handleSubmit = (event) => {
const { dispatch, mfa_token } = this.props;
const { code } = this.getFormData(event.target);
dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => {
this.setState({ code_error: false });
return dispatch(verifyCredentials(access_token));
}).then(account => {
this.setState({ shouldRedirect: true });
return dispatch(switchAccount(account.id));
}).catch(error => {
this.setState({ isLoading: false, code_error: true });
});
this.setState({ isLoading: true });
event.preventDefault();
}
render() {
const { intl } = this.props;
const { code_error, shouldRedirect } = this.state;
if (shouldRedirect) return <Redirect to='/' />;
return (
<div>
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 border-solid -mx-4 sm:-mx-10'>
<h1 className='text-center font-bold text-2xl'>
<FormattedMessage id='login.otp_log_in' defaultMessage='OTP Login' />
</h1>
</div>
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
<Form onSubmit={this.handleSubmit}>
<FormGroup
labelText={intl.formatMessage(messages.otpCodeLabel)}
hintText={intl.formatMessage(messages.otpCodeHint)}
errors={code_error ? [intl.formatMessage(messages.otpLoginFail)] : []}
>
<Input
name='code'
type='text'
autoComplete='off'
onChange={this.onInputChange}
autoFocus
required
/>
</FormGroup>
<FormActions>
<Button
theme='primary'
type='submit'
disabled={this.state.isLoading}
>
<FormattedMessage id='login.sign_in' defaultMessage='Sign in' />
</Button>
</FormActions>
</Form>
</div>
</div>
);
}
}

@ -0,0 +1,88 @@
import React, { useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { Redirect } from 'react-router-dom';
import { otpVerify, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
const messages = defineMessages({
otpCodeHint: { id: 'login.fields.otp_code_hint', defaultMessage: 'Enter the two-factor code generated by your phone app or use one of your recovery codes' },
otpCodeLabel: { id: 'login.fields.otp_code_label', defaultMessage: 'Two-factor code:' },
otpLoginFail: { id: 'login.otp_log_in.fail', defaultMessage: 'Invalid code, please try again.' },
});
interface IOtpAuthForm {
mfa_token: string,
}
const OtpAuthForm: React.FC<IOtpAuthForm> = ({ mfa_token }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const [isLoading, setIsLoading] = useState(false);
const [shouldRedirect, setShouldRedirect] = useState(false);
const [codeError, setCodeError] = useState<string | boolean>('');
const getFormData = (form: any) => Object.fromEntries(
Array.from(form).map((i: any) => [i.name, i.value]),
);
const handleSubmit = (event: React.FormEvent<Element>) => {
const { code } = getFormData(event.target);
dispatch(otpVerify(code, mfa_token)).then(({ access_token }) => {
setCodeError(false);
return dispatch(verifyCredentials(access_token));
}).then(account => {
setShouldRedirect(true);
return dispatch(switchAccount(account.id));
}).catch(() => {
setIsLoading(false);
setCodeError(true);
});
setIsLoading(true);
event.preventDefault();
};
if (shouldRedirect) return <Redirect to='/' />;
return (
<div>
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 border-solid -mx-4 sm:-mx-10'>
<h1 className='text-center font-bold text-2xl'>
<FormattedMessage id='login.otp_log_in' defaultMessage='OTP Login' />
</h1>
</div>
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
<Form onSubmit={handleSubmit}>
<FormGroup
labelText={intl.formatMessage(messages.otpCodeLabel)}
hintText={intl.formatMessage(messages.otpCodeHint)}
errors={codeError ? [intl.formatMessage(messages.otpLoginFail)] : []}
>
<Input
name='code'
type='text'
autoComplete='off'
autoFocus
required
/>
</FormGroup>
<FormActions>
<Button
theme='primary'
type='submit'
disabled={isLoading}
>
<FormattedMessage id='login.sign_in' defaultMessage='Sign in' />
</Button>
</FormActions>
</Form>
</div>
</div>
);
};
export default OtpAuthForm;

@ -1,72 +0,0 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { resetPassword } from 'soapbox/actions/security';
import snackbar from 'soapbox/actions/snackbar';
import { Button, Form, FormActions, FormGroup, Input } from '../../../components/ui';
const messages = defineMessages({
nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' },
confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' },
});
export default @connect()
@injectIntl
class PasswordReset extends ImmutablePureComponent {
state = {
isLoading: false,
success: false,
}
handleSubmit = e => {
const { dispatch, intl } = this.props;
const nicknameOrEmail = e.target.nickname_or_email.value;
this.setState({ isLoading: true });
dispatch(resetPassword(nicknameOrEmail)).then(() => {
this.setState({ isLoading: false, success: true });
dispatch(snackbar.info(intl.formatMessage(messages.confirmation)));
}).catch(error => {
this.setState({ isLoading: false });
});
}
render() {
const { intl } = this.props;
if (this.state.success) return <Redirect to='/' />;
return (
<div>
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-600 border-solid -mx-4 sm:-mx-10'>
<h1 className='text-center font-bold text-2xl'>
<FormattedMessage id='password_reset.header' defaultMessage='Reset Password' />
</h1>
</div>
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
<Form onSubmit={this.handleSubmit}>
<FormGroup labelText={intl.formatMessage(messages.nicknameOrEmail)}>
<Input
name='nickname_or_email'
placeholder='me@example.com'
required
/>
</FormGroup>
<FormActions>
<Button type='submit' theme='primary' disabled={this.state.isLoading}>
<FormattedMessage id='password_reset.reset' defaultMessage='Reset password' />
</Button>
</FormActions>
</Form>
</div>
</div>
);
}
}

@ -0,0 +1,66 @@
import React, { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Redirect } from 'react-router-dom';
import { resetPassword } from 'soapbox/actions/security';
import snackbar from 'soapbox/actions/snackbar';
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
const messages = defineMessages({
nicknameOrEmail: { id: 'password_reset.fields.username_placeholder', defaultMessage: 'Email or username' },
confirmation: { id: 'password_reset.confirmation', defaultMessage: 'Check your email for confirmation.' },
});
const PasswordReset = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const [isLoading, setIsLoading] = useState(false);
const [success, setSuccess] = useState(false);
const handleSubmit = (e: React.FormEvent<Element>) => {
const nicknameOrEmail = (e.target as any).nickname_or_email.value;
setIsLoading(true);
dispatch(resetPassword(nicknameOrEmail)).then(() => {
setIsLoading(false);
setSuccess(true);
dispatch(snackbar.info(intl.formatMessage(messages.confirmation)));
}).catch(() => {
setIsLoading(false);
});
};
if (success) return <Redirect to='/' />;
return (
<div>
<div className='pb-4 sm:pb-10 mb-4 border-b border-gray-200 dark:border-gray-600 border-solid -mx-4 sm:-mx-10'>
<h1 className='text-center font-bold text-2xl'>
<FormattedMessage id='password_reset.header' defaultMessage='Reset Password' />
</h1>
</div>
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
<Form onSubmit={handleSubmit}>
<FormGroup labelText={intl.formatMessage(messages.nicknameOrEmail)}>
<Input
type='text'
name='nickname_or_email'
placeholder='me@example.com'
required
/>
</FormGroup>
<FormActions>
<Button type='submit' theme='primary' disabled={isLoading}>
<FormattedMessage id='password_reset.reset' defaultMessage='Reset password' />
</Button>
</FormActions>
</Form>
</div>
</div>
);
};
export default PasswordReset;

@ -1,5 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { defineMessages, FormattedDate, useIntl } from 'react-intl';
import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security';

@ -11,6 +11,7 @@ import { connect } from 'react-redux';
import AutosuggestInput from 'soapbox/components/autosuggest_input';
import Icon from 'soapbox/components/icon';
import IconButton from 'soapbox/components/icon_button';
import { HStack } from 'soapbox/components/ui';
const messages = defineMessages({
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
@ -177,7 +178,7 @@ class PollForm extends ImmutablePureComponent {
))}
</ul>
<div className='poll__footer'>
<HStack className='text-black dark:text-white' space={2}>
{options.size < maxOptions && (
<button className='button button-secondary' onClick={this.handleAddOption}><Icon src={require('@tabler/icons/icons/plus.svg')} /> <FormattedMessage {...messages.add_option} /></button>
)}
@ -191,7 +192,7 @@ class PollForm extends ImmutablePureComponent {
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
</select>
</div>
</HStack>
</div>
);
}

@ -8,6 +8,7 @@ import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import Icon from 'soapbox/components/icon';
import { Text } from 'soapbox/components/ui';
const hasRestrictions = remoteInstance => {
return remoteInstance
@ -49,77 +50,57 @@ class InstanceRestrictions extends ImmutablePureComponent {
if (followers_only) {
items.push((
<div className='federation-restriction' key='followers_only'>
<div className='federation-restriction__icon'>
<Icon src={require('@tabler/icons/icons/lock.svg')} />
</div>
<div className='federation-restriction__message'>
<FormattedMessage
id='federation_restriction.followers_only'
defaultMessage='Hidden except to followers'
/>
</div>
</div>
<Text key='followers_only'>
<Icon className='mr-2' src={require('@tabler/icons/icons/lock.svg')} />
<FormattedMessage
id='federation_restriction.followers_only'
defaultMessage='Hidden except to followers'
/>
</Text>
));
} else if (federated_timeline_removal) {
items.push((
<div className='federation-restriction' key='federated_timeline_removal'>
<div className='federation-restriction__icon'>
<Icon src={require('@tabler/icons/icons/lock-open.svg')} />
</div>
<div className='federation-restriction__message'>
<FormattedMessage
id='federation_restriction.federated_timeline_removal'
defaultMessage='Fediverse timeline removal'
/>
</div>
</div>
<Text key='federated_timeline_removal'>
<Icon className='mr-2' src={require('@tabler/icons/icons/lock-open.svg')} />
<FormattedMessage
id='federation_restriction.federated_timeline_removal'
defaultMessage='Fediverse timeline removal'
/>
</Text>
));
}
if (fullMediaRemoval) {
items.push((
<div className='federation-restriction' key='full_media_removal'>
<div className='federation-restriction__icon'>
<Icon src={require('@tabler/icons/icons/photo-off.svg')} />
</div>
<div className='federation-restriction__message'>
<FormattedMessage
id='federation_restriction.full_media_removal'
defaultMessage='Full media removal'
/>
</div>
</div>
<Text key='full_media_removal'>
<Icon className='mr-2' src={require('@tabler/icons/icons/photo-off.svg')} />
<FormattedMessage
id='federation_restriction.full_media_removal'
defaultMessage='Full media removal'
/>
</Text>
));
} else if (partialMediaRemoval) {
items.push((
<div className='federation-restriction' key='partial_media_removal'>
<div className='federation-restriction__icon'>
<Icon src={require('@tabler/icons/icons/photo-off.svg')} />
</div>
<div className='federation-restriction__message'>
<FormattedMessage
id='federation_restriction.partial_media_removal'
defaultMessage='Partial media removal'
/>
</div>
</div>
<Text key='partial_media_removal'>
<Icon className='mr-2' src={require('@tabler/icons/icons/photo-off.svg')} />
<FormattedMessage
id='federation_restriction.partial_media_removal'
defaultMessage='Partial media removal'
/>
</Text>
));
}
if (!fullMediaRemoval && media_nsfw) {
items.push((
<div className='federation-restriction' key='media_nsfw'>
<div className='federation-restriction__icon'>
<Icon id='eye-slash' />
</div>
<div className='federation-restriction__message'>
<FormattedMessage
id='federation_restriction.media_nsfw'
defaultMessage='Attachments marked NSFW'
/>
</div>
</div>
<Text key='media_nsfw'>
<Icon className='mr-2' id='eye-slash' />
<FormattedMessage
id='federation_restriction.media_nsfw'
defaultMessage='Attachments marked NSFW'
/>
</Text>
));
}
@ -135,38 +116,38 @@ class InstanceRestrictions extends ImmutablePureComponent {
if (remoteInstance.getIn(['federation', 'reject']) === true) {
return (
<div className='instance-restrictions__message'>
<Icon id='times' />
<Text>
<Icon className='mr-2' id='times' />
<FormattedMessage
id='remote_instance.federation_panel.restricted_message'
defaultMessage='{siteTitle} blocks all activities from {host}.'
values={{ host, siteTitle }}
/>
</div>
</Text>
);
} else if (hasRestrictions(remoteInstance)) {
return [
(
<div className='instance-restrictions__message'>
<Text>
<FormattedMessage
id='remote_instance.federation_panel.some_restrictions_message'
defaultMessage='{siteTitle} has placed some restrictions on {host}.'
values={{ host, siteTitle }}
/>
</div>
</Text>
),
this.renderRestrictions(),
];
} else {
return (
<div className='instance-restrictions__message'>
<Icon id='check' />
<Text>
<Icon className='mr-2' id='check' />
<FormattedMessage
id='remote_instance.federation_panel.no_restrictions_message'
defaultMessage='{siteTitle} has placed no restrictions on {host}.'
values={{ host, siteTitle }}
/>
</div>
</Text>
);
}
}

@ -1,5 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';

@ -1,5 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';

@ -1,5 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';

@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { openModal } from '../../actions/modals';
const mapDispatchToProps = dispatch => ({
onLoad: (text) => {
dispatch(openModal('COMPOSE'));
},
});
export default @connect(null, mapDispatchToProps)
class NewStatus extends React.Component {
static propTypes = {
onLoad: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.props.onLoad();
}
render() {
return (
<Redirect to='/' />
);
}
}

@ -0,0 +1,20 @@
import React from 'react';
import { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';
import { useAppDispatch } from 'soapbox/hooks';
const NewStatus = () => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(openModal('COMPOSE'));
}, []);
return (
<Redirect to='/' />
);
};
export default NewStatus;

@ -1,9 +1,8 @@
'use strict';
import classNames from 'classnames';
import React from 'react';
import { Link } from 'react-router-dom';
import { Button, HStack } from 'soapbox/components/ui';
import { useSettings } from 'soapbox/hooks';
interface IPinnedHostsPicker {
@ -18,13 +17,18 @@ const PinnedHostsPicker: React.FC<IPinnedHostsPicker> = ({ host: activeHost }) =
if (!pinnedHosts || pinnedHosts.isEmpty()) return null;
return (
<div className='pinned-hosts-picker'>
<HStack className='mb-4' space={2}>
{pinnedHosts.map((host: any) => (
<div className={classNames('pinned-host', { 'active': host === activeHost })} key={host}>
<Link to={`/timeline/${host}`}>{host}</Link>
</div>
<Button
key={host}
to={`/timeline/${host}`}
size='sm'
theme={host === activeHost ? 'accent' : 'secondary'}
>
{host}
</Button>
))}
</div>
</HStack>
);
};

@ -3,6 +3,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import IconButton from 'soapbox/components/icon_button';
import { HStack, Text } from 'soapbox/components/ui';
import Column from 'soapbox/features/ui/components/column';
import { useAppDispatch, useSettings } from 'soapbox/hooks';
@ -66,14 +67,16 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
return (
<Column label={intl.formatMessage(messages.title)} heading={instance} transparent>
{instance && <PinnedHostsPicker host={instance} />}
{!pinned && <div className='timeline-filter-message'>
<IconButton src={require('@tabler/icons/icons/x.svg')} onClick={handleCloseClick} />
<FormattedMessage
id='remote_timeline.filter_message'
defaultMessage='You are viewing the timeline of {instance}.'
values={{ instance }}
/>
</div>}
{!pinned && <HStack className='mb-4 px-2' space={2}>
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/icons/x.svg')} onClick={handleCloseClick} />
<Text>
<FormattedMessage
id='remote_timeline.filter_message'
defaultMessage='You are viewing the timeline of {instance}.'
values={{ instance }}
/>
</Text>
</HStack>}
<StatusListContainer
scrollKey={`${timelineId}_${instance}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}

@ -1,94 +0,0 @@
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 { fetchAccount } from 'soapbox/actions/accounts';
import { addToMentions, removeFromMentions } from 'soapbox/actions/compose';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name';
import IconButton from 'soapbox/components/icon_button';
import { makeGetAccount } from 'soapbox/selectors';
const messages = defineMessages({
remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' },
add: { id: 'reply_mentions.account.add', defaultMessage: 'Add to mentions' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => {
const account = getAccount(state, accountId);
return {
added: !!account && state.getIn(['compose', 'to']).includes(account.get('acct')),
account,
};
};
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { accountId }) => ({
onRemove: () => dispatch(removeFromMentions(accountId)),
onAdd: () => dispatch(addToMentions(accountId)),
fetchAccount: () => dispatch(fetchAccount(accountId)),
});
export default @connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
onRemove: PropTypes.func.isRequired,
onAdd: PropTypes.func.isRequired,
added: PropTypes.bool,
author: PropTypes.bool,
};
static defaultProps = {
added: false,
};
componentDidMount() {
const { account, accountId } = this.props;
if (accountId && !account) {
this.props.fetchAccount(accountId);
}
}
render() {
const { account, intl, onRemove, onAdd, added, author } = this.props;
if (!account) return null;
let button;
if (added) {
button = <IconButton src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
}
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</div>
<div className='account__relationship'>
{!author && button}
</div>
</div>
</div>
);
}
}

@ -0,0 +1,67 @@
import React from 'react';
import { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { fetchAccount } from 'soapbox/actions/accounts';
import { addToMentions, removeFromMentions } from 'soapbox/actions/compose';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name';
import IconButton from 'soapbox/components/icon_button';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
const messages = defineMessages({
remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' },
add: { id: 'reply_mentions.account.add', defaultMessage: 'Add to mentions' },
});
const getAccount = makeGetAccount();
interface IAccount {
accountId: string,
author: boolean,
}
const Account: React.FC<IAccount> = ({ accountId, author }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => getAccount(state, accountId));
const added = useAppSelector((state) => !!account && state.compose.get('to').includes(account.acct));
const onRemove = () => dispatch(removeFromMentions(accountId));
const onAdd = () => dispatch(addToMentions(accountId));
useEffect(() => {
if (accountId && !account) {
dispatch(fetchAccount(accountId));
}
}, []);
if (!account) return null;
let button;
if (added) {
button = <IconButton src={require('@tabler/icons/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
} else {
button = <IconButton src={require('@tabler/icons/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
}
return (
<div className='account'>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</div>
<div className='account__relationship'>
{!author && button}
</div>
</div>
</div>
);
};
export default Account;

@ -2,8 +2,8 @@ import classNames from 'classnames';
import React from 'react';
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import StatusContent from 'soapbox/components/status_content';
import StatusReplyMentions from 'soapbox/components/status_reply_mentions';
import { HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import PollPreview from 'soapbox/features/ui/components/poll_preview';

@ -1,6 +1,5 @@
import { debounce } from 'lodash';
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchScheduledStatuses, expandScheduledStatuses } from 'soapbox/actions/scheduled_statuses';

@ -1,48 +0,0 @@
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 Column from '../ui/components/column';
import LinkFooter from '../ui/components/link_footer';
import PromoPanel from '../ui/components/promo_panel';
const messages = defineMessages({
heading: { id: 'column.info', defaultMessage: 'Server information' },
});
const mapStateToProps = (state, props) => ({
instance: state.get('instance'),
});
export default @connect(mapStateToProps)
@injectIntl
class ServerInfo extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
};
render() {
const { intl, instance } = this.props;
return (
<Column icon='info' label={intl.formatMessage(messages.heading)}>
<div className='info_column_area'>
<div className='info__brand'>
<div className='brand'>
<h1>{instance.get('title')}</h1>
</div>
<div className='brand__tagline'>
<span>{instance.get('description')}</span>
</div>
</div>
<PromoPanel />
<LinkFooter />
</div>
</Column>
);
}
}

@ -0,0 +1,36 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useAppSelector } from 'soapbox/hooks';
import Column from '../ui/components/column';
import LinkFooter from '../ui/components/link_footer';
import PromoPanel from '../ui/components/promo_panel';
const messages = defineMessages({
heading: { id: 'column.info', defaultMessage: 'Server information' },
});
const ServerInfo = () => {
const intl = useIntl();
const instance = useAppSelector((state) => state.instance);
return (
<Column icon='info' label={intl.formatMessage(messages.heading)}>
<div className='info_column_area'>
<div className='info__brand'>
<div className='brand'>
<h1>{instance.title}</h1>
</div>
<div className='brand__tagline'>
<span>{instance.description}</span>
</div>
</div>
<PromoPanel />
<LinkFooter />
</div>
</Column>
);
};
export default ServerInfo;

@ -1,16 +1,11 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { getSettings, changeSettingImmediate } from 'soapbox/actions/settings';
import {
SimpleForm,
SelectDropdown,
} from 'soapbox/features/forms';
import { useAppSelector } from 'soapbox/hooks';
import List, { ListItem } from '../../components/list';
import { Card, CardBody, CardHeader, CardTitle } from '../../components/ui';
import List, { ListItem } from 'soapbox/components/list';
import { Card, CardBody, CardHeader, CardTitle } from 'soapbox/components/ui';
import { SimpleForm, SelectDropdown } from 'soapbox/features/forms';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
mediaDisplay: { id: 'preferences.fields.media_display_label', defaultMessage: 'Media display' },
@ -20,7 +15,7 @@ const messages = defineMessages({
});
const MediaDisplay = () => {
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const intl = useIntl();
const settings = useAppSelector((state) => getSettings(state));
@ -31,7 +26,7 @@ const MediaDisplay = () => {
show_all: intl.formatMessage(messages.display_media_show_all),
};
const onSelectChange = path => {
const onSelectChange: (path: string[]) => React.ChangeEventHandler<HTMLSelectElement> = path => {
return e => {
dispatch(changeSettingImmediate(path, e.target.value));
};
@ -49,7 +44,7 @@ const MediaDisplay = () => {
<ListItem label={intl.formatMessage(messages.mediaDisplay)}>
<SelectDropdown
items={displayMediaOptions}
defaultValue={settings.get('displayMedia')}
defaultValue={settings.get('displayMedia') as string}
onChange={onSelectChange(['displayMedia'])}
/>
</ListItem>

@ -1,47 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { openComposeWithText } from '../../actions/compose';
const mapDispatchToProps = dispatch => ({
onShare: (text) => {
dispatch(openComposeWithText(text));
},
});
export default @connect(null, mapDispatchToProps)
class Share extends React.Component {
static propTypes = {
onShare: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
const params = new URLSearchParams(window.location.search);
const text = [
params.get('title'),
params.get('text'),
params.get('url'),
]
.filter(v => v)
.join('\n\n');
if (text) {
this.props.onShare(text);
}
}
render() {
return (
<Redirect to='/' />
);
}
}

@ -0,0 +1,30 @@
import React from 'react';
import { Redirect, useLocation } from 'react-router-dom';
import { openComposeWithText } from 'soapbox/actions/compose';
import { useAppDispatch } from 'soapbox/hooks';
const Share = () => {
const dispatch = useAppDispatch();
const { search } = useLocation();
const params = new URLSearchParams(search);
const text = [
params.get('title'),
params.get('text'),
params.get('url'),
]
.filter(v => v)
.join('\n\n');
if (text) {
dispatch(openComposeWithText(text));
}
return (
<Redirect to='/' />
);
};
export default Share;

@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import { defaultSettings } from 'soapbox/actions/settings';
import SiteLogo from 'soapbox/components/site-logo';
import BackgroundShapes from 'soapbox/features/ui/components/background_shapes';
import { useSystemTheme } from 'soapbox/hooks';
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
@ -47,7 +48,7 @@ const SitePreview: React.FC<ISitePreview> = ({ soapbox }) => {
'bg-slate-800': dark,
})}
>
<img alt='Logo' className='h-5 lg:h-6 self-center px-2' src={soapboxConfig.logo} />
<SiteLogo alt='Logo' className='h-5 lg:h-6 w-auto self-center px-2' theme={dark ? 'dark' : 'light'} />
</div>
</div>
);

@ -5,13 +5,13 @@ import { FormattedMessage, injectIntl, WrappedComponentProps as IntlProps } from
import { FormattedDate } from 'react-intl';
import Icon from 'soapbox/components/icon';
import MediaGallery from 'soapbox/components/media_gallery';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import StatusContent from 'soapbox/components/status_content';
import { HStack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
import MediaGallery from '../../../components/media_gallery';
import StatusContent from '../../../components/status_content';
import StatusReplyMentions from '../../../components/status_reply_mentions';
import { HStack, Text } from '../../../components/ui';
import AccountContainer from '../../../containers/account_container';
import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import Video from '../../video';

@ -582,7 +582,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
}
renderPendingStatus(id: string) {
const { status } = this.props;
// const { status } = this.props;
const idempotencyKey = id.replace(/^末pending-/, '');
return (
@ -590,10 +590,10 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
className='thread__status'
key={id}
idempotencyKey={idempotencyKey}
focusedStatusId={status.id}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
// focusedStatusId={status.id}
// onMoveUp={this.handleMoveUp}
// onMoveDown={this.handleMoveDown}
// contextType='thread'
/>
);
}

@ -1,26 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
export default class ComponentModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
component: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
componentProps: PropTypes.object,
}
static defaultProps = {
componentProps: {},
}
render() {
const { onClose, component: Component, componentProps } = this.props;
return (
<div className='modal-root__modal component-modal'>
<Component onClose={onClose} {...componentProps} />
</div>
);
}
}

@ -0,0 +1,19 @@
import React from 'react';
import { Modal } from 'soapbox/components/ui';
interface IComponentModal {
onClose: (type?: string) => void,
component: React.ComponentType<{
onClose: (type?: string) => void,
}>,
componentProps: Record<any, any>,
}
const ComponentModal: React.FC<IComponentModal> = ({ onClose, component: Component, componentProps = {} }) => (
<Modal onClose={onClose} title=''>
<Component onClose={onClose} {...componentProps} />
</Modal>
);
export default ComponentModal;

@ -1,17 +1,18 @@
import { Map as ImmutableMap } from 'immutable';
import React, { useState, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Toggle from 'react-toggle';
import { updateMrf } from 'soapbox/actions/mrf';
import snackbar from 'soapbox/actions/snackbar';
import { SimpleForm, Checkbox } from 'soapbox/features/forms';
import { HStack, Modal, Stack, Text } from 'soapbox/components/ui';
import { SimpleForm } from 'soapbox/features/forms';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { makeGetRemoteInstance } from 'soapbox/selectors';
const getRemoteInstance = makeGetRemoteInstance();
const messages = defineMessages({
reject: { id: 'edit_federation.reject', defaultMessage: 'Reject all activities' },
mediaRemoval: { id: 'edit_federation.media_removal', defaultMessage: 'Strip media' },
forceNsfw: { id: 'edit_federation.force_nsfw', defaultMessage: 'Force attachments to be marked sensitive' },
unlisted: { id: 'edit_federation.unlisted', defaultMessage: 'Force posts unlisted' },
@ -54,7 +55,7 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
setData(newData);
};
const handleSubmit: React.FormEventHandler = () => {
const handleSubmit = () => {
dispatch(updateMrf(host, data))
.then(() => dispatch(snackbar.success(intl.formatMessage(messages.success, { host }))))
.catch(() => {});
@ -75,47 +76,81 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
const fullMediaRemoval = avatar_removal && banner_removal && media_removal;
return (
<div className='modal-root__modal edit-federation-modal'>
<div>
<div className='edit-federation-modal__title'>
{host}
</div>
<SimpleForm onSubmit={handleSubmit}>
<Checkbox
label={intl.formatMessage(messages.reject)}
checked={reject}
onChange={handleDataChange('reject')}
/>
<Checkbox
label={intl.formatMessage(messages.mediaRemoval)}
disabled={reject}
checked={fullMediaRemoval}
onChange={handleMediaRemoval}
/>
<Checkbox
label={intl.formatMessage(messages.forceNsfw)}
disabled={reject || media_removal}
checked={media_nsfw}
onChange={handleDataChange('media_nsfw')}
/>
<Checkbox
label={intl.formatMessage(messages.followersOnly)}
disabled={reject}
checked={followers_only}
onChange={handleDataChange('followers_only')}
/>
<Checkbox
label={intl.formatMessage(messages.unlisted)}
disabled={reject || followers_only}
checked={federated_timeline_removal}
onChange={handleDataChange('federated_timeline_removal')}
/>
<button type='submit' className='edit-federation-modal__submit'>
{intl.formatMessage(messages.save)}
</button>
</SimpleForm>
</div>
</div>
<Modal
onClose={onClose}
title={host}
confirmationAction={handleSubmit}
confirmationText={intl.formatMessage(messages.save)}
>
<SimpleForm onSubmit={handleSubmit}>
<Stack space={2}>
<HStack space={2} alignItems='center'>
<Toggle
checked={reject}
onChange={handleDataChange('reject')}
icons={false}
id='reject'
/>
<Text theme='muted' tag='label' size='sm' htmlFor='reject'>
<FormattedMessage id='edit_federation.reject' defaultMessage='Reject all activities' />
</Text>
</HStack>
<HStack space={2} alignItems='center'>
<Toggle
checked={fullMediaRemoval}
onChange={handleMediaRemoval}
icons={false}
id='media_removal'
disabled={reject}
/>
<Text theme='muted' tag='label' size='sm' htmlFor='media_removal'>
<FormattedMessage id='edit_federation.media_removal' defaultMessage='Strip media' />
</Text>
</HStack>
<HStack space={2} alignItems='center'>
<Toggle
checked={media_nsfw}
onChange={handleDataChange('media_nsfw')}
icons={false}
id='media_nsfw'
disabled={reject || media_removal}
/>
<Text theme='muted' tag='label' size='sm' htmlFor='media_nsfw'>
<FormattedMessage id='edit_federation.force_nsfw' defaultMessage='Force attachments to be marked sensitive' />
</Text>
</HStack>
<HStack space={2} alignItems='center'>
<Toggle
checked={followers_only}
onChange={handleDataChange('followers_only')}
icons={false}
id='followers_only'
disabled={reject}
/>
<Text theme='muted' tag='label' size='sm' htmlFor='followers_only'>
<FormattedMessage id='edit_federation.followers_only' defaultMessage='Hide posts except to followers' />
</Text>
</HStack>
<HStack space={2} alignItems='center'>
<Toggle
checked={federated_timeline_removal}
onChange={handleDataChange('federated_timeline_removal')}
icons={false}
id='federated_timeline_removal'
disabled={reject || followers_only}
/>
<Text theme='muted' tag='label' size='sm' htmlFor='federated_timeline_removal'>
<FormattedMessage id='edit_federation.unlisted' defaultMessage='Force posts unlisted' />
</Text>
</HStack>
</Stack>
</SimpleForm>
</Modal>
);
};

@ -1,76 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { fetchFavourites } from 'soapbox/actions/interactions';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
const mapStateToProps = (state, props) => {
return {
accountIds: state.getIn(['user_lists', 'favourited_by', props.statusId]),
};
};
export default @connect(mapStateToProps)
class FavouritesModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
statusId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
};
fetchData = () => {
const { dispatch, statusId } = this.props;
dispatch(fetchFavourites(statusId));
}
componentDidMount() {
this.fetchData();
}
onClickClose = () => {
this.props.onClose('FAVOURITES');
};
render() {
const { accountIds } = this.props;
let body;
if (!accountIds) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has liked this post yet. When someone does, they will show up here.' />;
body = (
<ScrollableList
scrollKey='favourites'
emptyMessage={emptyMessage}
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.favourites' defaultMessage='likes' />}
onClose={this.onClickClose}
>
{body}
</Modal>
);
}
}

@ -0,0 +1,62 @@
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchFavourites } from 'soapbox/actions/interactions';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
interface IFavouritesModal {
onClose: (type: string) => void,
statusId: string,
}
const FavouritesModal: React.FC<IFavouritesModal> = ({ onClose, statusId }) => {
const dispatch = useAppDispatch();
const accountIds = useAppSelector((state) => state.user_lists.getIn(['favourited_by', statusId]));
const fetchData = () => {
dispatch(fetchFavourites(statusId));
};
useEffect(() => {
fetchData();
}, []);
const onClickClose = () => {
onClose('FAVOURITES');
};
let body;
if (!accountIds) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has liked this post yet. When someone does, they will show up here.' />;
body = (
<ScrollableList
scrollKey='favourites'
emptyMessage={emptyMessage}
itemClassName='pb-3'
>
{accountIds.map((id: string) =>
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.favourites' defaultMessage='Likes' />}
onClose={onClickClose}
>
{body}
</Modal>
);
};
export default FavouritesModal;

@ -4,7 +4,7 @@ import React from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { pinHost, unpinHost } from 'soapbox/actions/remote_timeline';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
import { Widget } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
import { makeGetRemoteInstance } from 'soapbox/selectors';
@ -29,7 +29,7 @@ const InstanceInfoPanel: React.FC<IInstanceInfoPanel> = ({ host }) => {
const remoteInstance: any = useAppSelector(state => getRemoteInstance(state, host));
const pinned: boolean = (settings.getIn(['remote_timeline', 'pinnedHosts']) as any).includes(host);
const handlePinHost: React.MouseEventHandler = () => {
const handlePinHost = () => {
if (!pinned) {
dispatch(pinHost(host));
} else {
@ -37,31 +37,15 @@ const InstanceInfoPanel: React.FC<IInstanceInfoPanel> = ({ host }) => {
}
};
const makeMenu = () => {
return [{
text: intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host }),
action: handlePinHost,
icon: require(pinned ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg'),
}];
};
const menu = makeMenu();
const icon = pinned ? 'thumbtack' : 'globe-w';
if (!remoteInstance) return null;
return (
<div className='wtf-panel instance-federation-panel'>
<div className='wtf-panel-header'>
<i role='img' className={`fa fa-${icon} wtf-panel-header__icon`} />
<span className='wtf-panel-header__label'>
<span>{remoteInstance.get('host')}</span>
</span>
<div className='wtf-panel__menu'>
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} />
</div>
</div>
</div>
<Widget
title={remoteInstance.get('host')}
onActionClick={handlePinHost}
actionIcon={require(pinned ? '@tabler/icons/icons/pinned-off.svg' : '@tabler/icons/icons/pin.svg')}
actionTitle={intl.formatMessage(pinned ? messages.unpinHost : messages.pinHost, { host })}
/>
);
};

@ -4,6 +4,7 @@ import React from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import { Widget } from 'soapbox/components/ui';
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions';
import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks';
@ -43,22 +44,14 @@ const InstanceModerationPanel: React.FC<IInstanceModerationPanel> = ({ host }) =
const menu = makeMenu();
return (
<div className='wtf-panel instance-federation-panel'>
<div className='wtf-panel-header'>
<i role='img' className='fa fa-gavel wtf-panel-header__icon' />
<span className='wtf-panel-header__label'>
<span><FormattedMessage id='remote_instance.federation_panel.heading' defaultMessage='Federation Restrictions' /></span>
</span>
{account?.admin && (
<div className='wtf-panel__menu'>
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} />
</div>
)}
</div>
<div className='wtf-panel__content'>
<InstanceRestrictions remoteInstance={remoteInstance} />
</div>
</div>
<Widget
title={<FormattedMessage id='remote_instance.federation_panel.heading' defaultMessage='Federation Restrictions' />}
action={account?.admin ? (
<DropdownMenu items={menu} src={require('@tabler/icons/icons/dots-vertical.svg')} />
) : undefined}
>
<InstanceRestrictions remoteInstance={remoteInstance} />
</Widget>
);
};

@ -1,83 +0,0 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { fetchStatusWithContext } from 'soapbox/actions/statuses';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { makeGetStatus } from 'soapbox/selectors';
const mapStateToProps = (state, props) => {
const getStatus = makeGetStatus();
const status = getStatus(state, {
id: props.statusId,
username: props.username,
});
return {
accountIds: status ? ImmutableOrderedSet(status.get('mentions').map(m => m.get('id'))) : null,
};
};
export default @connect(mapStateToProps)
@injectIntl
class MentionsModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
statusId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
};
fetchData = () => {
const { dispatch, statusId } = this.props;
dispatch(fetchStatusWithContext(statusId));
}
componentDidMount() {
this.fetchData();
}
onClickClose = () => {
this.props.onClose('MENTIONS');
};
render() {
const { accountIds } = this.props;
let body;
if (!accountIds) {
body = <Spinner />;
} else {
body = (
<ScrollableList
scrollKey='mentions'
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.mentions' defaultMessage='Mentions' />}
onClose={this.onClickClose}
>
{body}
</Modal>
);
}
}

@ -0,0 +1,64 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchStatusWithContext } from 'soapbox/actions/statuses';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetStatus } from 'soapbox/selectors';
const getStatus = makeGetStatus();
interface IMentionsModal {
onClose: (type: string) => void,
statusId: string,
}
const MentionsModal: React.FC<IMentionsModal> = ({ onClose, statusId }) => {
const dispatch = useAppDispatch();
const status = useAppSelector((state) => getStatus(state, { id: statusId }));
const accountIds = status ? ImmutableOrderedSet(status.mentions.map(m => m.get('id'))) : null;
const fetchData = () => {
dispatch(fetchStatusWithContext(statusId));
};
const onClickClose = () => {
onClose('MENTIONS');
};
useEffect(() => {
fetchData();
}, []);
let body;
if (!accountIds) {
body = <Spinner />;
} else {
body = (
<ScrollableList
scrollKey='mentions'
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.mentions' defaultMessage='Mentions' />}
onClose={onClickClose}
>
{body}
</Modal>
);
};
export default MentionsModal;

@ -1,95 +0,0 @@
import classNames from 'classnames';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import StatusContent from 'soapbox/components/status_content';
import StatusReplyMentions from 'soapbox/components/status_reply_mentions';
import { HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery';
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
import { buildStatus } from '../util/pending_status_builder';
import PollPreview from './poll_preview';
const shouldHaveCard = pendingStatus => {
return Boolean(pendingStatus.get('content').match(/https?:\/\/\S*/));
};
const mapStateToProps = (state, props) => {
const { idempotencyKey } = props;
const pendingStatus = state.getIn(['pending_statuses', idempotencyKey]);
return {
status: pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null,
};
};
export default @connect(mapStateToProps)
@injectIntl
class PendingStatus extends ImmutablePureComponent {
renderMedia = () => {
const { status } = this.props;
if (status.get('media_attachments') && !status.get('media_attachments').isEmpty()) {
return (
<PlaceholderMediaGallery
media={status.get('media_attachments')}
/>
);
} else if (!status.get('quote') && shouldHaveCard(status)) {
return <PlaceholderCard />;
} else {
return null;
}
}
render() {
const { status, className } = this.props;
if (!status) return null;
if (!status.get('account')) return null;
return (
<div className={classNames('opacity-50', className)}>
<div className={classNames('status', { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
<div className={classNames('status__wrapper', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id') })} tabIndex={this.props.muted ? null : 0}>
<div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer
key={String(status.getIn(['account', 'id']))}
id={String(status.getIn(['account', 'id']))}
timestamp={status.created_at}
timestampUrl={status.get('created_at')}
hideActions
/>
</HStack>
</div>
<div className='status__content-wrapper'>
<StatusReplyMentions status={status} />
<StatusContent
status={status}
expanded
collapsable
/>
{this.renderMedia()}
{status.get('poll') && <PollPreview poll={status.get('poll')} />}
{status.get('quote') && <QuotedStatus statusId={status.get('quote')} />}
</div>
{/* TODO */}
{/* <PlaceholderActionBar /> */}
</div>
</div>
</div>
);
}
}

@ -0,0 +1,97 @@
import classNames from 'classnames';
import React from 'react';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import StatusContent from 'soapbox/components/status_content';
import { HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder_media_gallery';
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
import { useAppSelector } from 'soapbox/hooks';
import { buildStatus } from '../util/pending_status_builder';
import PollPreview from './poll_preview';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
const shouldHaveCard = (pendingStatus: StatusEntity) => {
return Boolean(pendingStatus.content.match(/https?:\/\/\S*/));
};
interface IPendingStatus {
className?: string,
idempotencyKey: string,
muted?: boolean,
}
interface IPendingStatusMedia {
status: StatusEntity,
}
const PendingStatusMedia: React.FC<IPendingStatusMedia> = ({ status }) => {
if (status.media_attachments && !status.media_attachments.isEmpty()) {
return (
<PlaceholderMediaGallery
media={status.media_attachments}
/>
);
} else if (!status.quote && shouldHaveCard(status)) {
return <PlaceholderCard />;
} else {
return null;
}
};
const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, muted }) => {
const status = useAppSelector((state) => {
const pendingStatus = state.pending_statuses.get(idempotencyKey);
return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null;
}) as StatusEntity | null;
if (!status) return null;
if (!status.account) return null;
const account = status.account as AccountEntity;
return (
<div className={classNames('opacity-50', className)}>
<div className={classNames('status', { 'status-reply': !!status.in_reply_to_id, muted })} data-id={status.id}>
<div className={classNames('status__wrapper', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} tabIndex={muted ? undefined : 0}>
<div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer
key={account.id}
id={account.id}
timestamp={status.created_at}
hideActions
/>
</HStack>
</div>
<div className='status__content-wrapper'>
<StatusReplyMentions status={status} />
<StatusContent
status={status}
expanded
collapsable
/>
<PendingStatusMedia status={status} />
{status.poll && <PollPreview poll={status.poll} />}
{status.quote && <QuotedStatus statusId={status.quote} />}
</div>
{/* TODO */}
{/* <PlaceholderActionBar /> */}
</div>
</div>
</div>
);
};
export default PendingStatus;

@ -1,79 +0,0 @@
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 { connect } from 'react-redux';
import Icon from 'soapbox/components/icon';
import { fetchPinnedAccounts } from '../../../actions/accounts';
import AccountContainer from '../../../containers/account_container';
class PinnedAccountsPanel extends ImmutablePureComponent {
static propTypes = {
pinned: ImmutablePropTypes.list.isRequired,
fetchPinned: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
componentDidMount() {
this.props.fetchPinned();
}
render() {
const { account } = this.props;
const pinned = this.props.pinned.slice(0, this.props.limit);
if (pinned.isEmpty()) {
return null;
}
return (
<div className='wtf-panel'>
<div className='wtf-panel-header'>
<Icon src={require('@tabler/icons/icons/users.svg')} className='wtf-panel-header__icon' />
<span className='wtf-panel-header__label'>
<FormattedMessage
id='pinned_accounts.title'
defaultMessage='{name}s choices'
values={{
name: <span className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />,
}}
/>
</span>
</div>
<div className='wtf-panel__content'>
<div className='wtf-panel__list'>
{pinned && pinned.map(suggestion => (
<AccountContainer
key={suggestion}
id={suggestion}
withRelationship={false}
/>
))}
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state, { account }) => ({
pinned: state.getIn(['user_lists', 'pinned', account.get('id'), 'items'], ImmutableList()),
});
const mapDispatchToProps = (dispatch, { account }) => {
return {
fetchPinned: () => dispatch(fetchPinnedAccounts(account.get('id'))),
};
};
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
},
)(PinnedAccountsPanel));

@ -0,0 +1,50 @@
import { List as ImmutableList } from 'immutable';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchPinnedAccounts } from 'soapbox/actions/accounts';
import { Widget } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import type { Account } from 'soapbox/types/entities';
interface IPinnedAccountsPanel {
account: Account,
limit: number,
}
const PinnedAccountsPanel: React.FC<IPinnedAccountsPanel> = ({ account, limit }) => {
const dispatch = useAppDispatch();
const pinned = useAppSelector((state) => state.user_lists.getIn(['pinned', account.id, 'items'], ImmutableList())).slice(0, limit);
useEffect(() => {
dispatch(fetchPinnedAccounts(account.id));
}, []);
if (pinned.isEmpty()) {
return null;
}
return (
<Widget
title={<FormattedMessage
id='pinned_accounts.title'
defaultMessage='{name}s choices'
values={{
name: <span className='display-name__html' dangerouslySetInnerHTML={{ __html: account.display_name_html }} />,
}}
/>}
>
{pinned && pinned.map((suggestion: string) => (
<AccountContainer
key={suggestion}
id={suggestion}
withRelationship={false}
/>
))}
</Widget>
);
};
export default PinnedAccountsPanel;

@ -1,6 +1,5 @@
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { FormattedList, FormattedMessage } from 'react-intl';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';

@ -1,16 +1,14 @@
import { List as ImmutableList } from 'immutable';
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
import FilterBar from 'soapbox/components/filter_bar';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -20,14 +18,13 @@ const messages = defineMessages({
interface IReactionsModal {
onClose: (string: string) => void,
statusId: string,
username: string,
reaction?: string,
}
const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, ...props }) => {
const dispatch = useDispatch();
const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, reaction: initialReaction }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const [reaction, setReaction] = useState(props.reaction);
const [reaction, setReaction] = useState(initialReaction);
const reactions = useAppSelector<Array<{
accounts: Array<string>,
count: number,

@ -1,95 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { fetchReblogs } from 'soapbox/actions/interactions';
import { fetchStatus } from 'soapbox/actions/statuses';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
const mapStateToProps = (state, props) => {
return {
accountIds: state.getIn(['user_lists', 'reblogged_by', props.statusId]),
};
};
export default @connect(mapStateToProps)
@injectIntl
@withRouter
class ReblogsModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
statusId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired,
accountIds: ImmutablePropTypes.orderedSet,
history: PropTypes.object,
};
fetchData = () => {
const { dispatch, statusId } = this.props;
dispatch(fetchReblogs(statusId));
dispatch(fetchStatus(statusId));
}
componentDidMount() {
this.fetchData();
this.unlistenHistory = this.props.history.listen((_, action) => {
if (action === 'PUSH') {
this.onClickClose(null, true);
}
});
}
componentWillUnmount() {
if (this.unlistenHistory) {
this.unlistenHistory();
}
}
onClickClose = () => {
this.props.onClose('REBLOGS');
};
render() {
const { accountIds } = this.props;
let body;
if (!accountIds) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this post yet. When someone does, they will show up here.' />;
body = (
<ScrollableList
scrollKey='reblogs'
emptyMessage={emptyMessage}
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.reblogs' defaultMessage='Reposts' />}
onClose={this.onClickClose}
>
{body}
</Modal>
);
}
}

@ -0,0 +1,64 @@
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchReblogs } from 'soapbox/actions/interactions';
import { fetchStatus } from 'soapbox/actions/statuses';
import ScrollableList from 'soapbox/components/scrollable_list';
import { Modal, Spinner } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
interface IReblogsModal {
onClose: (string: string) => void,
statusId: string,
}
const ReblogsModal: React.FC<IReblogsModal> = ({ onClose, statusId }) => {
const dispatch = useAppDispatch();
const accountIds = useAppSelector((state) => state.user_lists.getIn(['reblogged_by', statusId]));
const fetchData = () => {
dispatch(fetchReblogs(statusId));
dispatch(fetchStatus(statusId));
};
useEffect(() => {
fetchData();
}, []);
const onClickClose = () => {
onClose('REBLOGS');
};
let body;
if (!accountIds) {
body = <Spinner />;
} else {
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this post yet. When someone does, they will show up here.' />;
body = (
<ScrollableList
scrollKey='reblogs'
emptyMessage={emptyMessage}
itemClassName='pb-3'
>
{accountIds.map((id: string) =>
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
);
}
return (
<Modal
title={<FormattedMessage id='column.reblogs' defaultMessage='Reposts' />}
onClose={onClickClose}
>
{body}
</Modal>
);
};
export default ReblogsModal;

@ -33,7 +33,7 @@ const ReplyMentionsModal: React.FC<IReplyMentionsModal> = ({ onClose }) => {
closePosition='left'
>
<div className='reply-mentions-modal__accounts'>
{mentions.map(accountId => <Account key={accountId} accountId={accountId} added author={author === accountId} />)}
{mentions.map(accountId => <Account key={accountId} accountId={accountId} author={author === accountId} />)}
</div>
</Modal>
);

@ -1,136 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import Avatar from 'soapbox/components/avatar';
import StillImage from 'soapbox/components/still_image';
import VerificationBadge from 'soapbox/components/verification_badge';
import { getAcct } from 'soapbox/utils/accounts';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import { displayFqn } from 'soapbox/utils/state';
import { HStack, Stack, Text } from '../../../components/ui';
import { makeGetAccount } from '../../../selectors';
class UserPanel extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.record,
displayFqn: PropTypes.bool,
intl: PropTypes.object.isRequired,
domain: PropTypes.string,
}
render() {
const { account, action, badges, displayFqn, intl, domain } = this.props;
if (!account) return null;
const displayNameHtml = { __html: account.get('display_name_html') };
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
const header = account.get('header');
const verified = account.get('verified');
return (
<div className='relative'>
<Stack space={2}>
<Stack>
<div className='-mt-4 -mx-4 h-24 bg-gray-200 relative'>
{header && (
<StillImage
src={account.get('header')}
className='absolute inset-0 object-cover'
alt=''
/>
)}
</div>
<HStack justifyContent='between'>
<Link
to={`/@${account.get('acct')}`}
title={acct}
className='-mt-12 block'
>
<Avatar
account={account}
className='h-20 w-20 bg-gray-50 ring-2 ring-white'
/>
</Link>
{action && (
<div className='mt-2'>{action}</div>
)}
</HStack>
</Stack>
<Stack>
<Link to={`/@${account.get('acct')}`}>
<HStack space={1} alignItems='center'>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
{verified && <VerificationBadge />}
{badges.length > 0 && (
<HStack space={1} alignItems='center'>
{badges}
</HStack>
)}
</HStack>
</Link>
<Text size='sm' theme='muted'>
@{getAcct(account, displayFqn)}
</Text>
</Stack>
<HStack alignItems='center' space={3}>
{account.get('followers_count') >= 0 && (
<Link to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<HStack alignItems='center' space={1}>
<Text theme='primary' weight='bold' size='sm'>
{shortNumberFormat(account.get('followers_count'))}
</Text>
<Text weight='bold' size='sm'>
<FormattedMessage id='account.followers' defaultMessage='Followers' />
</Text>
</HStack>
</Link>
)}
{account.get('following_count') >= 0 && (
<Link to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<HStack alignItems='center' space={1}>
<Text theme='primary' weight='bold' size='sm'>
{shortNumberFormat(account.get('following_count'))}
</Text>
<Text weight='bold' size='sm'>
<FormattedMessage id='account.follows' defaultMessage='Follows' />
</Text>
</HStack>
</Link>
)}
</HStack>
</Stack>
</div>
);
}
}
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId),
displayFqn: displayFqn(state),
});
return mapStateToProps;
};
export default injectIntl(
connect(makeMapStateToProps, null, null, {
forwardRef: true,
})(UserPanel));

@ -0,0 +1,119 @@
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import Avatar from 'soapbox/components/avatar';
import StillImage from 'soapbox/components/still_image';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import VerificationBadge from 'soapbox/components/verification_badge';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import { getAcct } from 'soapbox/utils/accounts';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import { displayFqn } from 'soapbox/utils/state';
const getAccount = makeGetAccount();
interface IUserPanel {
accountId: string,
action?: JSX.Element,
badges?: JSX.Element[],
domain?: string,
}
const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain }) => {
const intl = useIntl();
const account = useAppSelector((state) => getAccount(state, accountId));
const fqn = useAppSelector((state) => displayFqn(state));
if (!account) return null;
const displayNameHtml = { __html: account.get('display_name_html') };
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
const header = account.get('header');
const verified = account.get('verified');
return (
<div className='relative'>
<Stack space={2}>
<Stack>
<div className='-mt-4 -mx-4 h-24 bg-gray-200 relative'>
{header && (
<StillImage
src={account.get('header')}
className='absolute inset-0 object-cover'
alt=''
/>
)}
</div>
<HStack justifyContent='between'>
<Link
to={`/@${account.get('acct')}`}
title={acct}
className='-mt-12 block'
>
<Avatar
account={account}
className='h-20 w-20 bg-gray-50 ring-2 ring-white'
/>
</Link>
{action && (
<div className='mt-2'>{action}</div>
)}
</HStack>
</Stack>
<Stack>
<Link to={`/@${account.get('acct')}`}>
<HStack space={1} alignItems='center'>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={displayNameHtml} />
{verified && <VerificationBadge />}
{badges && badges.length > 0 && (
<HStack space={1} alignItems='center'>
{badges}
</HStack>
)}
</HStack>
</Link>
<Text size='sm' theme='muted'>
@{getAcct(account, fqn)}
</Text>
</Stack>
<HStack alignItems='center' space={3}>
{account.get('followers_count') >= 0 && (
<Link to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<HStack alignItems='center' space={1}>
<Text theme='primary' weight='bold' size='sm'>
{shortNumberFormat(account.get('followers_count'))}
</Text>
<Text weight='bold' size='sm'>
<FormattedMessage id='account.followers' defaultMessage='Followers' />
</Text>
</HStack>
</Link>
)}
{account.get('following_count') >= 0 && (
<Link to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<HStack alignItems='center' space={1}>
<Text theme='primary' weight='bold' size='sm'>
{shortNumberFormat(account.get('following_count'))}
</Text>
<Text weight='bold' size='sm'>
<FormattedMessage id='account.follows' defaultMessage='Follows' />
</Text>
</HStack>
</Link>
)}
</HStack>
</Stack>
</div>
);
};
export default UserPanel;

@ -4,9 +4,11 @@ import { normalizeStatus } from 'soapbox/normalizers/status';
import { calculateStatus } from 'soapbox/reducers/statuses';
import { makeGetAccount } from 'soapbox/selectors';
import type { RootState } from 'soapbox/store';
const getAccount = makeGetAccount();
const buildMentions = pendingStatus => {
const buildMentions = (pendingStatus: ImmutableMap<string, any>) => {
if (pendingStatus.get('in_reply_to_id')) {
return ImmutableList(pendingStatus.get('to') || []).map(acct => ImmutableMap({ acct }));
} else {
@ -14,18 +16,18 @@ const buildMentions = pendingStatus => {
}
};
const buildPoll = pendingStatus => {
const buildPoll = (pendingStatus: ImmutableMap<string, any>) => {
if (pendingStatus.hasIn(['poll', 'options'])) {
return pendingStatus.get('poll').update('options', options => {
return options.map(title => ImmutableMap({ title }));
return pendingStatus.get('poll').update('options', (options: ImmutableMap<string, any>) => {
return options.map((title: string) => ImmutableMap({ title }));
});
} else {
return null;
}
};
export const buildStatus = (state, pendingStatus, idempotencyKey) => {
const me = state.get('me');
export const buildStatus = (state: RootState, pendingStatus: ImmutableMap<string, any>, idempotencyKey: string) => {
const me = state.me as string;
const account = getAccount(state, me);
const inReplyToId = pendingStatus.get('in_reply_to_id');
@ -33,9 +35,9 @@ export const buildStatus = (state, pendingStatus, idempotencyKey) => {
account,
content: pendingStatus.get('status', '').replace(new RegExp('\n', 'g'), '<br>'), /* eslint-disable-line no-control-regex */
id: `末pending-${idempotencyKey}`,
in_reply_to_account_id: state.getIn(['statuses', inReplyToId, 'account'], null),
in_reply_to_account_id: state.statuses.getIn([inReplyToId, 'account'], null),
in_reply_to_id: inReplyToId,
media_attachments: pendingStatus.get('media_ids', ImmutableList()).map(id => ImmutableMap({ id })),
media_attachments: pendingStatus.get('media_ids', ImmutableList()).map((id: string) => ImmutableMap({ id })),
mentions: buildMentions(pendingStatus),
poll: buildPoll(pendingStatus),
quote: pendingStatus.get('quote_id', null),

@ -1,7 +1,8 @@
import PropTypes from 'prop-types';
import { AxiosError } from 'axios';
import * as React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useParams } from 'react-router-dom';
import snackbar from 'soapbox/actions/snackbar';
import { confirmEmailVerification } from 'soapbox/actions/verification';
@ -91,8 +92,8 @@ const TokenExpired = () => {
);
};
const EmailPassThru = ({ match }) => {
const { token } = match.params;
const EmailPassThru = () => {
const { token } = useParams<{ token: string }>();
const dispatch = useDispatch();
const intl = useIntl();
@ -106,7 +107,7 @@ const EmailPassThru = ({ match }) => {
setStatus(Statuses.SUCCESS);
dispatch(snackbar.success(intl.formatMessage({ id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' })));
})
.catch((error) => {
.catch((error: AxiosError<any>) => {
const errorKey = error?.response?.data?.error;
let message = intl.formatMessage({
id: 'email_passthru.fail.generic',
@ -155,8 +156,4 @@ const EmailPassThru = ({ match }) => {
}
};
EmailPassThru.propTypes = {
match: PropTypes.object,
};
export default EmailPassThru;

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
@ -12,15 +11,15 @@ import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
import { logOut } from '../../actions/auth';
import { Button, Stack, Text } from '../../components/ui';
const WaitlistPage = ({ account }) => {
const WaitlistPage = (/* { account } */) => {
const dispatch = useDispatch();
const intl = useIntl();
const title = useAppSelector((state) => state.instance.title);
const me = useOwnAccount();
const isSmsVerified = me.getIn(['source', 'sms_verified']);
const isSmsVerified = me?.source.get('sms_verified');
const onClickLogOut = (event) => {
const onClickLogOut: React.MouseEventHandler = (event) => {
event.preventDefault();
dispatch(logOut(intl));
};
@ -76,8 +75,4 @@ const WaitlistPage = ({ account }) => {
);
};
WaitlistPage.propTypes = {
account: PropTypes.object,
};
export default WaitlistPage;

@ -802,23 +802,6 @@
}
}
.timeline-filter-message {
display: flex;
align-items: center;
background-color: var(--brand-color--faint);
color: var(--primary-text-color);
padding: 15px 20px;
.icon-button {
margin: 2px 8px 2px 0;
.svg-icon {
height: 20px;
width: 20px;
}
}
}
.column--better {
.column__top {
display: flex;

@ -47,7 +47,7 @@
@apply focus-within:ring-primary-500 focus-within:ring-2;
a {
@apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 focus:hover:bg-slate-800 cursor-pointer;
@apply flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-800 focus:bg-gray-100 dark:focus:bg-slate-800 cursor-pointer;
> .svg-icon:first-child {
@apply h-5 w-5 mr-2.5 transition-none;

@ -133,10 +133,6 @@
line-height: 18px;
}
&__footer {
@apply pt-1.5 pb-[5px] text-black dark:text-white;
}
&__link {
display: inline;
background: transparent;
@ -180,18 +176,6 @@
padding: 10px;
}
.poll__footer {
border-top: 1px solid var(--foreground-color);
padding: 10px;
margin: -5px 0 0 -5px;
button,
select {
flex: 1 1 50%;
margin: 5px 0 0 5px;
}
}
.button.button-secondary {
@apply h-auto py-1.5 px-2.5 text-primary-600 dark:text-primary-400 border-primary-600;
}

Loading…
Cancel
Save