sforkowany z mirror/soapbox
commit
86438cc9fa
|
@ -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 }) => (
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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} />
|
||||
|
|
Ładowanie…
Reference in New Issue