Merge branch 'ts' into 'develop'

TypeScript, FC

See merge request soapbox-pub/soapbox-fe!1472
dnd
marcin mikołajczak 2022-05-30 18:01:09 +00:00
commit 86438cc9fa
28 zmienionych plików z 536 dodań i 696 usunięć

Wyświetl plik

@ -6,7 +6,7 @@ interface IRadioButton {
checked?: boolean,
name: string,
onChange: React.ChangeEventHandler<HTMLInputElement>,
label: JSX.Element,
label: React.ReactNode,
}
const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, label }) => (

Wyświetl plik

@ -1,6 +1,5 @@
import { OrderedSet as ImmutableOrderedSet, is } from 'immutable';
import React, { useState } from 'react';
import { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';

Wyświetl plik

@ -1,64 +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 { withRouter } from 'react-router-dom';
import StatusContainer from '../../../containers/status_container';
export default @withRouter
class Conversation extends ImmutablePureComponent {
static propTypes = {
conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired,
lastStatusId: PropTypes.string,
unread: PropTypes.bool.isRequired,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
markRead: PropTypes.func.isRequired,
history: PropTypes.object,
};
handleClick = () => {
if (!this.props.history) {
return;
}
const { lastStatusId, unread, markRead } = this.props;
if (unread) {
markRead();
}
this.props.history.push(`/statuses/${lastStatusId}`);
}
handleHotkeyMoveUp = () => {
this.props.onMoveUp(this.props.conversationId);
}
handleHotkeyMoveDown = () => {
this.props.onMoveDown(this.props.conversationId);
}
render() {
const { accounts, lastStatusId, unread } = this.props;
if (lastStatusId === null) {
return null;
}
return (
<StatusContainer
id={lastStatusId}
unread={unread}
otherAccounts={accounts}
onMoveUp={this.handleHotkeyMoveUp}
onMoveDown={this.handleHotkeyMoveDown}
onClick={this.handleClick}
/>
);
}
}

Wyświetl plik

@ -0,0 +1,63 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { markConversationRead } from 'soapbox/actions/conversations';
import StatusContainer from 'soapbox/containers/status_container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import type { Map as ImmutableMap } from 'immutable';
interface IConversation {
conversationId: string,
onMoveUp: (id: string) => void,
onMoveDown: (id: string) => void,
}
const Conversation: React.FC<IConversation> = ({ conversationId, onMoveUp, onMoveDown }) => {
const dispatch = useAppDispatch();
const history = useHistory();
const { accounts, unread, lastStatusId } = useAppSelector((state) => {
const conversation = state.conversations.get('items').find((x: ImmutableMap<string, any>) => x.get('id') === conversationId);
return {
accounts: conversation.get('accounts').map((accountId: string) => state.accounts.get(accountId, null)),
unread: conversation.get('unread'),
lastStatusId: conversation.get('last_status', null),
};
});
const handleClick = () => {
if (unread) {
dispatch(markConversationRead(conversationId));
}
history.push(`/statuses/${lastStatusId}`);
};
const handleHotkeyMoveUp = () => {
onMoveUp(conversationId);
};
const handleHotkeyMoveDown = () => {
onMoveDown(conversationId);
};
if (lastStatusId === null) {
return null;
}
return (
<StatusContainer
// @ts-ignore
id={lastStatusId}
unread={unread}
otherAccounts={accounts}
onMoveUp={handleHotkeyMoveUp}
onMoveDown={handleHotkeyMoveDown}
onClick={handleClick}
/>
);
};
export default Conversation;

Wyświetl plik

@ -1,79 +0,0 @@
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ScrollableList from '../../../components/scrollable_list';
import ConversationContainer from '../containers/conversation_container';
export default class ConversationsList extends ImmutablePureComponent {
static propTypes = {
conversations: ImmutablePropTypes.list.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onLoadMore: PropTypes.func,
};
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)
handleMoveUp = id => {
const elementIndex = this.getCurrentIndex(id) - 1;
this._selectChild(elementIndex);
}
handleMoveDown = id => {
const elementIndex = this.getCurrentIndex(id) + 1;
this._selectChild(elementIndex);
}
_selectChild(index) {
this.node.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
const element = document.querySelector(`#direct-list [data-index="${index}"] .focusable`);
if (element) {
element.focus();
}
},
});
}
setRef = c => {
this.node = c;
}
handleLoadOlder = debounce(() => {
const maxId = this.props.conversations.getIn([-1, 'id']);
if (maxId) this.props.onLoadMore(maxId);
}, 300, { leading: true })
render() {
const { conversations, isLoading, onLoadMore, ...other } = this.props;
return (
<ScrollableList
{...other}
onLoadMore={onLoadMore && this.handleLoadOlder}
id='direct-list'
scrollKey='direct'
ref={this.setRef}
isLoading={isLoading}
showLoading={isLoading && conversations.size === 0}
>
{conversations.map(item => (
<ConversationContainer
key={item.get('id')}
conversationId={item.get('id')}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/>
))}
</ScrollableList>
);
}
}

Wyświetl plik

@ -0,0 +1,74 @@
import { debounce } from 'lodash';
import React from 'react';
import { useRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { expandConversations } from 'soapbox/actions/conversations';
import ScrollableList from 'soapbox/components/scrollable_list';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import Conversation from '../components/conversation';
import type { VirtuosoHandle } from 'react-virtuoso';
const ConversationsList: React.FC = () => {
const dispatch = useAppDispatch();
const ref = useRef<VirtuosoHandle>(null);
const conversations = useAppSelector((state) => state.conversations.get('items'));
const isLoading = useAppSelector((state) => state.conversations.get('isLoading', true));
const getCurrentIndex = (id: string) => conversations.findIndex((x: any) => x.get('id') === id);
const handleMoveUp = (id: string) => {
const elementIndex = getCurrentIndex(id) - 1;
selectChild(elementIndex);
};
const handleMoveDown = (id: string) => {
const elementIndex = getCurrentIndex(id) + 1;
selectChild(elementIndex);
};
const selectChild = (index: number) => {
ref.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
const element = document.querySelector<HTMLDivElement>(`#direct-list [data-index="${index}"] .focusable`);
if (element) {
element.focus();
}
},
});
};
const handleLoadOlder = debounce(() => {
const maxId = conversations.getIn([-1, 'id']);
if (maxId) dispatch(expandConversations({ maxId }));
}, 300, { leading: true });
return (
<ScrollableList
onLoadMore={handleLoadOlder}
id='direct-list'
scrollKey='direct'
ref={ref}
isLoading={isLoading}
showLoading={isLoading && conversations.size === 0}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
>
{conversations.map((item: any) => (
<Conversation
key={item.get('id')}
conversationId={item.get('id')}
onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown}
/>
))}
</ScrollableList>
);
};
export default ConversationsList;

Wyświetl plik

@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import { markConversationRead } from '../../../actions/conversations';
import Conversation from '../components/conversation';
const mapStateToProps = (state, { conversationId }) => {
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
return {
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
unread: conversation.get('unread'),
lastStatusId: conversation.get('last_status', null),
};
};
const mapDispatchToProps = (dispatch, { conversationId }) => ({
markRead: () => dispatch(markConversationRead(conversationId)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);

Wyświetl plik

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { expandConversations } from '../../../actions/conversations';
import ConversationsList from '../components/conversations_list';
const mapStateToProps = state => ({
conversations: state.getIn(['conversations', 'items']),
isLoading: state.getIn(['conversations', 'isLoading'], true),
hasMore: state.getIn(['conversations', 'hasMore'], false),
});
const mapDispatchToProps = dispatch => ({
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
});
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);

Wyświetl plik

@ -1,75 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { directComposeById } from 'soapbox/actions/compose';
import AccountSearch from 'soapbox/components/account_search';
import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
import { connectDirectStream } from '../../actions/streaming';
import { Column } from '../../components/ui';
import ConversationsListContainer from './containers/conversations_list_container';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
});
export default @connect()
@injectIntl
class ConversationsTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
};
componentDidMount() {
const { dispatch } = this.props;
dispatch(mountConversations());
dispatch(expandConversations());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount() {
this.props.dispatch(unmountConversations());
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
handleSuggestion = accountId => {
this.props.dispatch(directComposeById(accountId));
}
handleLoadMore = maxId => {
this.props.dispatch(expandConversations({ maxId }));
}
render() {
const { intl } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={this.handleSuggestion}
/>
<ConversationsListContainer
scrollKey='direct_timeline'
timelineId='direct'
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
/>
</Column>
);
}
}

Wyświetl plik

@ -0,0 +1,50 @@
import React, { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { directComposeById } from 'soapbox/actions/compose';
import { mountConversations, unmountConversations, expandConversations } from 'soapbox/actions/conversations';
import { connectDirectStream } from 'soapbox/actions/streaming';
import AccountSearch from 'soapbox/components/account_search';
import { Column } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import ConversationsList from './components/conversations_list';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
});
const ConversationsTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(mountConversations());
dispatch(expandConversations());
const disconnect = dispatch(connectDirectStream());
return () => {
dispatch(unmountConversations());
disconnect();
};
}, []);
const handleSuggestion = (accountId: string) => {
dispatch(directComposeById(accountId));
};
return (
<Column label={intl.formatMessage(messages.title)}>
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={handleSuggestion}
/>
<ConversationsList />
</Column>
);
};
export default ConversationsTimeline;

Wyświetl plik

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

Wyświetl plik

@ -1,84 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { directComposeById } from 'soapbox/actions/compose';
import AccountSearch from 'soapbox/components/account_search';
import { connectDirectStream } from '../../actions/streaming';
import { expandDirectTimeline } from '../../actions/timelines';
import ColumnHeader from '../../components/column_header';
import { Column } from '../../components/ui';
import StatusListContainer from '../ui/containers/status_list_container';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
});
export default @connect(mapStateToProps)
@injectIntl
class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
};
componentDidMount() {
const { dispatch } = this.props;
dispatch(expandDirectTimeline());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount() {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
handleSuggestion = accountId => {
this.props.dispatch(directComposeById(accountId));
}
handleLoadMore = maxId => {
this.props.dispatch(expandDirectTimeline({ maxId }));
}
render() {
const { intl, hasUnread } = this.props;
return (
<Column label={intl.formatMessage(messages.title)} transparent>
<ColumnHeader
icon='envelope'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
/>
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={this.handleSuggestion}
/>
<StatusListContainer
scrollKey='direct_timeline'
timelineId='direct'
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
divideType='space'
/>
</Column>
);
}
}

Wyświetl plik

@ -0,0 +1,66 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { directComposeById } from 'soapbox/actions/compose';
import { connectDirectStream } from 'soapbox/actions/streaming';
import { expandDirectTimeline } from 'soapbox/actions/timelines';
import AccountSearch from 'soapbox/components/account_search';
import ColumnHeader from 'soapbox/components/column_header';
import { Column } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import StatusListContainer from '../ui/containers/status_list_container';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
});
const DirectTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const hasUnread = useAppSelector((state) => state.timelines.getIn(['direct', 'unread']) > 0);
useEffect(() => {
dispatch(expandDirectTimeline());
const disconnect = dispatch(connectDirectStream());
return (() => {
disconnect();
});
}, []);
const handleSuggestion = (accountId: string) => {
dispatch(directComposeById(accountId));
};
const handleLoadMore = (maxId: string) => {
dispatch(expandDirectTimeline({ maxId }));
};
return (
<Column label={intl.formatMessage(messages.title)} transparent>
<ColumnHeader
icon='envelope'
active={hasUnread}
title={intl.formatMessage(messages.title)}
/>
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={handleSuggestion}
/>
<StatusListContainer
scrollKey='direct_timeline'
timelineId='direct'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
divideType='space'
/>
</Column>
);
};
export default DirectTimeline;

Wyświetl plik

@ -1,85 +0,0 @@
import classNames from 'classnames';
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 { getSettings } from 'soapbox/actions/settings';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name';
import Permalink from 'soapbox/components/permalink';
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
import { Text } from 'soapbox/components/ui';
import ActionButton from 'soapbox/features/ui/components/action-button';
import { makeGetAccount } from 'soapbox/selectors';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { id }) => ({
me: state.get('me'),
account: getAccount(state, id),
autoPlayGif: getSettings(state).get('autoPlayGif'),
});
return mapStateToProps;
};
export default @injectIntl
@connect(makeMapStateToProps)
class AccountCard extends ImmutablePureComponent {
static propTypes = {
me: SoapboxPropTypes.me,
account: ImmutablePropTypes.record.isRequired,
autoPlayGif: PropTypes.bool,
};
render() {
const { account, autoPlayGif, me } = this.props;
const followedBy = me !== account.get('id') && account.getIn(['relationship', 'followed_by']);
return (
<div className='directory__card'>
{followedBy &&
<div className='directory__card__info'>
<span className='relationship-tag'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>
</div>}
<div className='directory__card__action-button'>
<ActionButton account={account} small />
</div>
<div className='directory__card__img'>
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
</div>
<div className='directory__card__bar'>
<Permalink className='directory__card__bar__name' href={account.get('url')} to={`/@${account.get('acct')}`}>
<Avatar account={account} size={48} />
<DisplayName account={account} />
</Permalink>
</div>
<div className='directory__card__extra'>
<Text
className={classNames('account__header__content', (account.get('note').length === 0 || account.get('note') === '<p></p>') && 'empty')}
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
</div>
<div className='directory__card__extra'>
<div className='accounts-table__count'><Text theme='primary' size='sm'>{shortNumberFormat(account.get('statuses_count'))}</Text> <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
<div className='accounts-table__count'><Text theme='primary' size='sm'>{shortNumberFormat(account.get('followers_count'))}</Text> <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
<div className='accounts-table__count'>{account.get('last_status_at') === null ? <Text theme='primary' size='sm'><FormattedMessage id='account.never_active' defaultMessage='Never' /></Text> : <RelativeTimestamp className='text-primary-600 dark:text-primary-400' timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
</div>
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,82 @@
import classNames from 'classnames';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { getSettings } from 'soapbox/actions/settings';
import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display-name';
import Permalink from 'soapbox/components/permalink';
import RelativeTimestamp from 'soapbox/components/relative_timestamp';
import { Text } from 'soapbox/components/ui';
import ActionButton from 'soapbox/features/ui/components/action-button';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
import { shortNumberFormat } from 'soapbox/utils/numbers';
const getAccount = makeGetAccount();
interface IAccountCard {
id: string,
}
const AccountCard: React.FC<IAccountCard> = ({ id }) => {
const me = useAppSelector((state) => state.me);
const account = useAppSelector((state) => getAccount(state, id));
const autoPlayGif = useAppSelector((state) => getSettings(state).get('autoPlayGif'));
if (!account) return null;
const followedBy = me !== account.id && account.relationship.get('followed_by');
return (
<div className='directory__card'>
{followedBy &&
<div className='directory__card__info'>
<span className='relationship-tag'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>
</div>}
<div className='directory__card__action-button'>
<ActionButton account={account} small />
</div>
<div className='directory__card__img'>
<img src={autoPlayGif ? account.header : account.header_static} alt='' className='parallax' />
</div>
<div className='directory__card__bar'>
<Permalink className='directory__card__bar__name' href={account.url} to={`/@${account.acct}`}>
<Avatar account={account} size={48} />
<DisplayName account={account} />
</Permalink>
</div>
<div className='directory__card__extra'>
<Text
className={classNames('account__header__content', (account.note.length === 0 || account.note === '<p></p>') && 'empty')}
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
/>
</div>
<div className='directory__card__extra'>
<div className='accounts-table__count'>
<Text theme='primary' size='sm'>
{shortNumberFormat(account.statuses_count)}
</Text> <small><FormattedMessage id='account.posts' defaultMessage='Posts' /></small>
</div>
<div className='accounts-table__count'>
<Text theme='primary' size='sm'>
{shortNumberFormat(account.followers_count)}
</Text> <small><FormattedMessage id='account.followers' defaultMessage='Followers' />
</small>
</div>
<div className='accounts-table__count'>
{account.last_status_at === null
? <Text theme='primary' size='sm'><FormattedMessage id='account.never_active' defaultMessage='Never' /></Text>
: <RelativeTimestamp className='text-primary-600 dark:text-primary-400' timestamp={account.last_status_at} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small>
</div>
</div>
</div>
);
};
export default AccountCard;

Wyświetl plik

@ -1,116 +0,0 @@
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
import LoadMore from 'soapbox/components/load_more';
import RadioButton from 'soapbox/components/radio_button';
import Column from 'soapbox/features/ui/components/column';
import { getFeatures } from 'soapbox/utils/features';
import AccountCard from './components/account_card';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
});
const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
title: state.getIn(['instance', 'title']),
features: getFeatures(state.get('instance')),
});
export default @connect(mapStateToProps)
@injectIntl
class Directory extends React.PureComponent {
static propTypes = {
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
params: PropTypes.shape({
order: PropTypes.string,
local: PropTypes.bool,
}),
features: PropTypes.object.isRequired,
};
state = {
order: null,
local: null,
};
getParams = (props, state) => ({
order: state.order === null ? (props.params.order || 'active') : state.order,
local: state.local === null ? (props.params.local || false) : state.local,
});
componentDidMount() {
const { dispatch } = this.props;
dispatch(fetchDirectory(this.getParams(this.props, this.state)));
}
componentDidUpdate(prevProps, prevState) {
const { dispatch } = this.props;
const paramsOld = this.getParams(prevProps, prevState);
const paramsNew = this.getParams(this.props, this.state);
if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
dispatch(fetchDirectory(paramsNew));
}
}
handleChangeOrder = e => {
this.setState({ order: e.target.value });
}
handleChangeLocal = e => {
this.setState({ local: e.target.value === '1' });
}
handleLoadMore = () => {
const { dispatch } = this.props;
dispatch(expandDirectory(this.getParams(this.props, this.state)));
}
render() {
const { isLoading, accountIds, intl, title, features } = this.props;
const { order, local } = this.getParams(this.props, this.state);
return (
<Column icon='address-book-o' label={intl.formatMessage(messages.title)}>
<div className='directory__filter-form'>
<div className='directory__filter-form__column' role='group'>
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
</div>
{features.federating && (
<div className='directory__filter-form__column' role='group'>
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={this.handleChangeLocal} />
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
</div>
)}
</div>
<div className={classNames('directory__list', { loading: isLoading })}>
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
</div>
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
</Column>
);
}
}

Wyświetl plik

@ -0,0 +1,80 @@
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { fetchDirectory, expandDirectory } from 'soapbox/actions/directory';
import LoadMore from 'soapbox/components/load_more';
import RadioButton from 'soapbox/components/radio_button';
import Column from 'soapbox/features/ui/components/column';
import { useAppSelector } from 'soapbox/hooks';
import { getFeatures } from 'soapbox/utils/features';
import AccountCard from './components/account_card';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
});
const Directory = () => {
const intl = useIntl();
const dispatch = useDispatch();
const { search } = useLocation();
const params = new URLSearchParams(search);
const accountIds = useAppSelector((state) => state.user_lists.getIn(['directory', 'items'], ImmutableList()));
const isLoading = useAppSelector((state) => state.user_lists.getIn(['directory', 'isLoading'], true));
const title = useAppSelector((state) => state.instance.get('title'));
const features = useAppSelector((state) => getFeatures(state.instance));
const [order, setOrder] = useState(params.get('order') || 'active');
const [local, setLocal] = useState(!!params.get('local'));
useEffect(() => {
dispatch(fetchDirectory({ order: order || 'active', local: local || false }));
}, [order, local]);
const handleChangeOrder: React.ChangeEventHandler<HTMLInputElement> = e => {
setOrder(e.target.value);
};
const handleChangeLocal: React.ChangeEventHandler<HTMLInputElement> = e => {
setLocal(e.target.value === '1');
};
const handleLoadMore = () => {
dispatch(expandDirectory({ order: order || 'active', local: local || false }));
};
return (
<Column icon='address-book-o' label={intl.formatMessage(messages.title)}>
<div className='directory__filter-form'>
<div className='directory__filter-form__column' role='group'>
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={handleChangeOrder} />
<RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={handleChangeOrder} />
</div>
{features.federating && (
<div className='directory__filter-form__column' role='group'>
<RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain: title })} checked={local} onChange={handleChangeLocal} />
<RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={handleChangeLocal} />
</div>
)}
</div>
<div className={classNames('directory__list', { loading: isLoading })}>
{accountIds.map((accountId: string) => <AccountCard id={accountId} key={accountId} />)}
</div>
<LoadMore onClick={handleLoadMore} visible={!isLoading} />
</Column>
);
};
export default Directory;

Wyświetl plik

@ -1,11 +1,10 @@
import * as React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { changePassword } from 'soapbox/actions/security';
import snackbar from 'soapbox/actions/snackbar';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from '../../components/ui';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
const messages = defineMessages({
updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' },
@ -22,7 +21,7 @@ const initialState = { currentPassword: '', newPassword: '', newPasswordConfirma
const EditPassword = () => {
const intl = useIntl();
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const [state, setState] = React.useState(initialState);
const [isLoading, setLoading] = React.useState(false);

Wyświetl plik

@ -1,5 +1,4 @@
import React from 'react';
import { useState } from 'react';
import React, { useState } from 'react';
import { MessageDescriptor, useIntl } from 'react-intl';
import { Button, Form, FormActions, Text } from 'soapbox/components/ui';

Wyświetl plik

@ -1,61 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import Icon from 'soapbox/components/icon';
import { makeGetRemoteInstance } from 'soapbox/selectors';
import InstanceRestrictions from './instance_restrictions';
const getRemoteInstance = makeGetRemoteInstance();
const mapStateToProps = (state, ownProps) => {
return {
remoteInstance: getRemoteInstance(state, ownProps.host),
};
};
export default @connect(mapStateToProps)
class RestrictedInstance extends ImmutablePureComponent {
static propTypes = {
host: PropTypes.string.isRequired,
}
state = {
expanded: false,
}
toggleExpanded = e => {
this.setState({ expanded: !this.state.expanded });
e.preventDefault();
}
render() {
const { remoteInstance } = this.props;
const { expanded } = this.state;
return (
<div className={classNames('restricted-instance', {
'restricted-instance--reject': remoteInstance.getIn(['federation', 'reject']),
'restricted-instance--expanded': expanded,
})}
>
<a href='#' className='restricted-instance__header' onClick={this.toggleExpanded}>
<div className='restricted-instance__icon'>
<Icon src={expanded ? require('@tabler/icons/icons/caret-down.svg') : require('@tabler/icons/icons/caret-right.svg')} />
</div>
<div className='restricted-instance__host'>
{remoteInstance.get('host')}
</div>
</a>
<div className='restricted-instance__restrictions'>
<InstanceRestrictions remoteInstance={remoteInstance} />
</div>
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,47 @@
import classNames from 'classnames';
import React, { useState } from 'react';
import Icon from 'soapbox/components/icon';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetRemoteInstance } from 'soapbox/selectors';
import InstanceRestrictions from './instance_restrictions';
const getRemoteInstance = makeGetRemoteInstance();
interface IRestrictedInstance {
host: string,
}
const RestrictedInstance: React.FC<IRestrictedInstance> = ({ host }) => {
const remoteInstance: any = useAppSelector((state) => getRemoteInstance(state, host));
const [expanded, setExpanded] = useState(false);
const toggleExpanded: React.MouseEventHandler<HTMLAnchorElement> = e => {
setExpanded((value) => !value);
e.preventDefault();
};
return (
<div className={classNames('restricted-instance', {
'restricted-instance--reject': remoteInstance.getIn(['federation', 'reject']),
'restricted-instance--expanded': expanded,
})}
>
<a href='#' className='restricted-instance__header' onClick={toggleExpanded}>
<div className='restricted-instance__icon'>
<Icon src={expanded ? require('@tabler/icons/icons/caret-down.svg') : require('@tabler/icons/icons/caret-right.svg')} />
</div>
<div className='restricted-instance__host'>
{remoteInstance.get('host')}
</div>
</a>
<div className='restricted-instance__restrictions'>
<InstanceRestrictions remoteInstance={remoteInstance} />
</div>
</div>
);
};
export default RestrictedInstance;

Wyświetl plik

@ -1,76 +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 ScrollableList from 'soapbox/components/scrollable_list';
import Accordion from 'soapbox/features/ui/components/accordion';
import { makeGetHosts } from 'soapbox/selectors';
import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
import Column from '../ui/components/column';
import RestrictedInstance from './components/restricted_instance';
const messages = defineMessages({
heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' },
boxTitle: { id: 'federation_restrictions.explanation_box.title', defaultMessage: 'Instance-specific policies' },
boxMessage: { id: 'federation_restrictions.explanation_box.message', defaultMessage: 'Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.' },
emptyMessage: { id: 'federation_restrictions.empty_message', defaultMessage: '{siteTitle} has not restricted any instances.' },
notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' },
});
const getHosts = makeGetHosts();
const mapStateToProps = state => ({
siteTitle: state.getIn(['instance', 'title']),
hosts: getHosts(state),
disclosed: federationRestrictionsDisclosed(state),
});
export default @connect(mapStateToProps)
@injectIntl
class FederationRestrictions extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
disclosed: PropTypes.bool,
};
state = {
explanationBoxExpanded: true,
}
toggleExplanationBox = setting => {
this.setState({ explanationBoxExpanded: setting });
}
render() {
const { intl, hosts, siteTitle, disclosed } = this.props;
const { explanationBoxExpanded } = this.state;
const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed;
return (
<Column icon='gavel' label={intl.formatMessage(messages.heading)}>
<div className='explanation-box'>
<Accordion
headline={intl.formatMessage(messages.boxTitle)}
expanded={explanationBoxExpanded}
onToggle={this.toggleExplanationBox}
>
{intl.formatMessage(messages.boxMessage, { siteTitle })}
</Accordion>
</div>
<div className='federation-restrictions'>
<ScrollableList emptyMessage={intl.formatMessage(emptyMessage, { siteTitle })}>
{hosts.map(host => <RestrictedInstance key={host} host={host} />)}
</ScrollableList>
</div>
</Column>
);
}
}

Wyświetl plik

@ -0,0 +1,62 @@
import React, { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import ScrollableList from 'soapbox/components/scrollable_list';
import Accordion from 'soapbox/features/ui/components/accordion';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetHosts } from 'soapbox/selectors';
import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
import Column from '../ui/components/column';
import RestrictedInstance from './components/restricted_instance';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
const messages = defineMessages({
heading: { id: 'column.federation_restrictions', defaultMessage: 'Federation Restrictions' },
boxTitle: { id: 'federation_restrictions.explanation_box.title', defaultMessage: 'Instance-specific policies' },
boxMessage: { id: 'federation_restrictions.explanation_box.message', defaultMessage: 'Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.' },
emptyMessage: { id: 'federation_restrictions.empty_message', defaultMessage: '{siteTitle} has not restricted any instances.' },
notDisclosed: { id: 'federation_restrictions.not_disclosed_message', defaultMessage: '{siteTitle} does not disclose federation restrictions through the API.' },
});
const getHosts = makeGetHosts();
const FederationRestrictions = () => {
const intl = useIntl();
const siteTitle = useAppSelector((state) => state.instance.get('title'));
const hosts = useAppSelector((state) => getHosts(state)) as ImmutableOrderedSet<string>;
const disclosed = useAppSelector((state) => federationRestrictionsDisclosed(state));
const [explanationBoxExpanded, setExplanationBoxExpanded] = useState(true);
const toggleExplanationBox = (setting: boolean) => {
setExplanationBoxExpanded(setting);
};
const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed;
return (
<Column icon='gavel' label={intl.formatMessage(messages.heading)}>
<div className='explanation-box'>
<Accordion
headline={intl.formatMessage(messages.boxTitle)}
expanded={explanationBoxExpanded}
onToggle={toggleExplanationBox}
>
{intl.formatMessage(messages.boxMessage, { siteTitle })}
</Accordion>
</div>
<div className='federation-restrictions'>
<ScrollableList emptyMessage={intl.formatMessage(emptyMessage, { siteTitle })}>
{hosts.map((host) => <RestrictedInstance key={host} host={host} />)}
</ScrollableList>
</div>
</Column>
);
};
export default FederationRestrictions;

Wyświetl plik

@ -1,5 +1,4 @@
import React from 'react';
import { useState } from 'react';
import React, { useState } from 'react';
import { MessageDescriptor, useIntl } from 'react-intl';
import { Button, FileInput, Form, FormActions, FormGroup, Text } from 'soapbox/components/ui';

Wyświetl plik

@ -1,5 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals';

Wyświetl plik

@ -1,5 +1,4 @@
import React from 'react';
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { fetchAccount } from 'soapbox/actions/accounts';

Wyświetl plik

@ -1,6 +1,5 @@
import { List as ImmutableList } from 'immutable';
import React, { useEffect } from 'react';
import { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';

Wyświetl plik

@ -282,7 +282,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
<WrappedRoute path='/statuses/:statusId' exact page={StatusPage} component={Status} content={children} />
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />