Merge remote-tracking branch 'origin/develop' into feed-insertion-algorithm
This commit is contained in:
commit
0bf6dad97f
|
@ -8,9 +8,10 @@ import type { SearchFilter } from 'soapbox/reducers/search';
|
|||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
const SEARCH_CHANGE = 'SEARCH_CHANGE';
|
||||
const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||
const SEARCH_SHOW = 'SEARCH_SHOW';
|
||||
const SEARCH_CHANGE = 'SEARCH_CHANGE';
|
||||
const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||
const SEARCH_SHOW = 'SEARCH_SHOW';
|
||||
const SEARCH_RESULTS_CLEAR = 'SEARCH_RESULTS_CLEAR';
|
||||
|
||||
const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
|
||||
const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
|
||||
|
@ -28,7 +29,11 @@ const changeSearch = (value: string) =>
|
|||
(dispatch: AppDispatch) => {
|
||||
// If backspaced all the way, clear the search
|
||||
if (value.length === 0) {
|
||||
return dispatch(clearSearch());
|
||||
dispatch(clearSearchResults());
|
||||
return dispatch({
|
||||
type: SEARCH_CHANGE,
|
||||
value,
|
||||
});
|
||||
} else {
|
||||
return dispatch({
|
||||
type: SEARCH_CHANGE,
|
||||
|
@ -41,6 +46,10 @@ const clearSearch = () => ({
|
|||
type: SEARCH_CLEAR,
|
||||
});
|
||||
|
||||
const clearSearchResults = () => ({
|
||||
type: SEARCH_RESULTS_CLEAR,
|
||||
});
|
||||
|
||||
const submitSearch = (filter?: SearchFilter) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const value = getState().search.value;
|
||||
|
@ -167,6 +176,7 @@ export {
|
|||
SEARCH_CHANGE,
|
||||
SEARCH_CLEAR,
|
||||
SEARCH_SHOW,
|
||||
SEARCH_RESULTS_CLEAR,
|
||||
SEARCH_FETCH_REQUEST,
|
||||
SEARCH_FETCH_SUCCESS,
|
||||
SEARCH_FETCH_FAIL,
|
||||
|
@ -177,6 +187,7 @@ export {
|
|||
SEARCH_ACCOUNT_SET,
|
||||
changeSearch,
|
||||
clearSearch,
|
||||
clearSearchResults,
|
||||
submitSearch,
|
||||
fetchSearchRequest,
|
||||
fetchSearchSuccess,
|
||||
|
|
|
@ -74,6 +74,7 @@ const messages = defineMessages({
|
|||
suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
|
||||
unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
|
||||
search: { id: 'account.search', defaultMessage: 'Search from @{name}' },
|
||||
searchSelf: { id: 'account.search_self', defaultMessage: 'Search your posts' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
|
@ -378,6 +379,13 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
to: '/settings',
|
||||
icon: require('@tabler/icons/settings.svg'),
|
||||
});
|
||||
if (features.searchFromAccount) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.searchSelf, { name: account.username }),
|
||||
action: onSearch,
|
||||
icon: require('@tabler/icons/search.svg'),
|
||||
});
|
||||
}
|
||||
menu.push(null);
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.mutes),
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import {
|
||||
changeSearch,
|
||||
clearSearch,
|
||||
clearSearchResults,
|
||||
setSearchAccount,
|
||||
showSearch,
|
||||
submitSearch,
|
||||
|
@ -72,7 +73,7 @@ const Search = (props: ISearch) => {
|
|||
event.preventDefault();
|
||||
|
||||
if (value.length > 0 || submitted) {
|
||||
dispatch(clearSearch());
|
||||
dispatch(clearSearchResults());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from 'clsx';
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { clearSearch, expandSearch, setFilter } from 'soapbox/actions/search';
|
||||
import { expandSearch, setFilter, setSearchAccount } from 'soapbox/actions/search';
|
||||
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
|
||||
import Hashtag from 'soapbox/components/hashtag';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
@ -43,7 +43,7 @@ const SearchResults = () => {
|
|||
|
||||
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
||||
|
||||
const handleClearSearch = () => dispatch(clearSearch());
|
||||
const handleUnsetAccount = () => dispatch(setSearchAccount(null));
|
||||
|
||||
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter));
|
||||
|
||||
|
@ -196,7 +196,7 @@ const SearchResults = () => {
|
|||
<>
|
||||
{filterByAccount ? (
|
||||
<HStack className='mb-4 pb-4 px-2 border-solid border-b border-gray-200 dark:border-gray-800' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleClearSearch} />
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleUnsetAccount} />
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='search_results.filter_message'
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { changeValue, submit, reset } from '../../../actions/group_editor';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'groups.form.title', defaultMessage: 'Enter a new group title' },
|
||||
description: { id: 'groups.form.description', defaultMessage: 'Enter the group description' },
|
||||
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload a banner image' },
|
||||
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
|
||||
create: { id: 'groups.form.create', defaultMessage: 'Create group' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
title: state.getIn(['group_editor', 'title']),
|
||||
description: state.getIn(['group_editor', 'description']),
|
||||
coverImage: state.getIn(['group_editor', 'coverImage']),
|
||||
disabled: state.getIn(['group_editor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onTitleChange: value => dispatch(changeValue('title', value)),
|
||||
onDescriptionChange: value => dispatch(changeValue('description', value)),
|
||||
onCoverImageChange: value => dispatch(changeValue('coverImage', value)),
|
||||
onSubmit: routerHistory => dispatch(submit(routerHistory)),
|
||||
reset: () => dispatch(reset()),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
@withRouter
|
||||
class Create extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
coverImage: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onCoverImageChange: PropTypes.func.isRequired,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.reset();
|
||||
}
|
||||
|
||||
handleTitleChange = e => {
|
||||
this.props.onTitleChange(e.target.value);
|
||||
}
|
||||
|
||||
handleDescriptionChange = e => {
|
||||
this.props.onDescriptionChange(e.target.value);
|
||||
}
|
||||
|
||||
handleCoverImageChange = e => {
|
||||
this.props.onCoverImageChange(e.target.files[0]);
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit(this.props.history);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, description, coverImage, disabled, intl } = this.props;
|
||||
|
||||
return (
|
||||
<form className='group-form' method='post' onSubmit={this.handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
className='standard'
|
||||
type='text'
|
||||
value={title}
|
||||
disabled={disabled}
|
||||
onChange={this.handleTitleChange}
|
||||
placeholder={intl.formatMessage(messages.title)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
className='standard'
|
||||
type='text'
|
||||
value={description}
|
||||
disabled={disabled}
|
||||
onChange={this.handleDescriptionChange}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor='group_cover_image' className={classNames('group-form__file-label', { 'group-form__file-label--selected': coverImage !== null })}>
|
||||
{intl.formatMessage(coverImage === null ? messages.coverImage : messages.coverImageChange)}
|
||||
</label>
|
||||
<input
|
||||
type='file'
|
||||
className='group-form__file'
|
||||
id='group_cover_image'
|
||||
disabled={disabled}
|
||||
onChange={this.handleCoverImageChange}
|
||||
/>
|
||||
<button className='standard-small'>{intl.formatMessage(messages.create)}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
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 { withRouter } from 'react-router-dom';
|
||||
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
|
||||
import { changeValue, submit, setUp } from '../../../actions/group_editor';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'groups.form.title', defaultMessage: 'Title' },
|
||||
description: { id: 'groups.form.description', defaultMessage: 'Description' },
|
||||
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload new banner image (optional)' },
|
||||
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
|
||||
update: { id: 'groups.form.update', defaultMessage: 'Update group' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
title: state.getIn(['group_editor', 'title']),
|
||||
description: state.getIn(['group_editor', 'description']),
|
||||
coverImage: state.getIn(['group_editor', 'coverImage']),
|
||||
disabled: state.getIn(['group_editor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onTitleChange: value => dispatch(changeValue('title', value)),
|
||||
onDescriptionChange: value => dispatch(changeValue('description', value)),
|
||||
onCoverImageChange: value => dispatch(changeValue('coverImage', value)),
|
||||
onSubmit: routerHistory => dispatch(submit(routerHistory)),
|
||||
setUp: group => dispatch(setUp(group)),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
@withRouter
|
||||
class Edit extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
coverImage: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onDescriptionChange: PropTypes.func.isRequired,
|
||||
onCoverImageChange: PropTypes.func.isRequired,
|
||||
setUp: PropTypes.func.isRequired,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
if (props.group) props.setUp(props.group);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.group && this.props.group) {
|
||||
this.props.setUp(this.props.group);
|
||||
}
|
||||
}
|
||||
|
||||
handleTitleChange = e => {
|
||||
this.props.onTitleChange(e.target.value);
|
||||
}
|
||||
|
||||
handleDescriptionChange = e => {
|
||||
this.props.onDescriptionChange(e.target.value);
|
||||
}
|
||||
|
||||
handleCoverImageChange = e => {
|
||||
this.props.onCoverImageChange(e.target.files[0]);
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit(this.props.history);
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit(this.props.history);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { group, title, description, coverImage, disabled, intl } = this.props;
|
||||
|
||||
if (typeof group === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
} else if (group === false) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className='group-form' method='post' onSubmit={this.handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
className='standard'
|
||||
type='text'
|
||||
value={title}
|
||||
disabled={disabled}
|
||||
onChange={this.handleTitleChange}
|
||||
placeholder={intl.formatMessage(messages.title)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea
|
||||
className='standard'
|
||||
type='text'
|
||||
value={description}
|
||||
disabled={disabled}
|
||||
onChange={this.handleDescriptionChange}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor='group_cover_image' className={classNames('group-form__file-label', { 'group-form__file-label--selected': coverImage !== null })}>
|
||||
{intl.formatMessage(coverImage === null ? messages.coverImage : messages.coverImageChange)}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type='file'
|
||||
className='group-form__file'
|
||||
id='group_cover_image'
|
||||
disabled={disabled}
|
||||
onChange={this.handleCoverImageChange}
|
||||
/>
|
||||
|
||||
<button>{intl.formatMessage(messages.update)}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { shortNumberFormat } from '../../../utils/numbers';
|
||||
|
||||
const messages = defineMessages({
|
||||
members: { id: 'groups.card.members', defaultMessage: 'Members' },
|
||||
view: { id: 'groups.card.view', defaultMessage: 'View' },
|
||||
join: { id: 'groups.card.join', defaultMessage: 'Join' },
|
||||
role_member: { id: 'groups.card.roles.member', defaultMessage: 'You\'re a member' },
|
||||
role_admin: { id: 'groups.card.roles.admin', defaultMessage: 'You\'re an admin' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
relationships: state.getIn(['group_relationships', id]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class GroupCard extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
}
|
||||
|
||||
getRole() {
|
||||
const { intl, relationships } = this.props;
|
||||
|
||||
if (relationships.get('admin')) return intl.formatMessage(messages.role_admin);
|
||||
if (relationships.get('member')) return intl.formatMessage(messages.role_member);
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, group } = this.props;
|
||||
const coverImageUrl = group.get('cover_image_url');
|
||||
const role = this.getRole();
|
||||
|
||||
return (
|
||||
<Link to={`/groups/${group.get('id')}`} className='group-card'>
|
||||
<div className='group-card__header'>{coverImageUrl && <img alt='' src={coverImageUrl} />}</div>
|
||||
<div className='group-card__content'>
|
||||
<div className='group-card__title'>{group.get('title')}</div>
|
||||
<div className='group-card__meta'><strong>{shortNumberFormat(group.get('member_count'))}</strong> {intl.formatMessage(messages.members)}{role && <span> · {role}</span>}</div>
|
||||
<div className='group-card__description'>{group.get('description')}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchGroups } from '../../../actions/groups';
|
||||
import GroupCreate from '../create';
|
||||
|
||||
import GroupCard from './card';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||
create: { id: 'groups.create', defaultMessage: 'Create group' },
|
||||
tab_featured: { id: 'groups.tab_featured', defaultMessage: 'Featured' },
|
||||
tab_member: { id: 'groups.tab_member', defaultMessage: 'Member' },
|
||||
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Manage' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { activeTab }) => ({
|
||||
groupIds: state.getIn(['group_lists', activeTab]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Groups extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
showCreateForm: PropTypes.bool,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
groups: ImmutablePropTypes.map,
|
||||
groupIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps) {
|
||||
if (this.props.activeTab && this.props.activeTab !== oldProps.activeTab) {
|
||||
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||
}
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const { intl, activeTab } = this.props;
|
||||
|
||||
return (
|
||||
<div className='group-column-header'>
|
||||
<div className='group-column-header__cta'><Link to='/groups/create' className='button standard-small'>{intl.formatMessage(messages.create)}</Link></div>
|
||||
<div className='group-column-header__title'>{intl.formatMessage(messages.heading)}</div>
|
||||
|
||||
<div className='column-header__wrapper'>
|
||||
<h1 className='column-header'>
|
||||
<Link to='/groups' className={classNames('btn grouped', { 'active': 'featured' === activeTab })}>
|
||||
{intl.formatMessage(messages.tab_featured)}
|
||||
</Link>
|
||||
|
||||
<Link to='/groups/browse/member' className={classNames('btn grouped', { 'active': 'member' === activeTab })}>
|
||||
{intl.formatMessage(messages.tab_member)}
|
||||
</Link>
|
||||
|
||||
<Link to='/groups/browse/admin' className={classNames('btn grouped', { 'active': 'admin' === activeTab })}>
|
||||
{intl.formatMessage(messages.tab_admin)}
|
||||
</Link>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { groupIds, showCreateForm } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!showCreateForm && this.renderHeader()}
|
||||
{showCreateForm && <GroupCreate /> }
|
||||
|
||||
<div className='group-card-list'>
|
||||
{groupIds.map(id => <GroupCard key={id} id={id} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
|
||||
import {
|
||||
fetchMembers,
|
||||
expandMembers,
|
||||
} from '../../../actions/groups';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Column from '../../ui/components/column';
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.user_lists.groups.get(id)?.items,
|
||||
hasMore: !!state.user_lists.groups.get(id)?.next,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class GroupMembers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchMembers(id));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.params.id !== prevProps.params.id) {
|
||||
this.props.dispatch(fetchMembers(this.props.params.id));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandMembers(this.props.params.id));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render() {
|
||||
const { accountIds, hasMore, group } = this.props;
|
||||
|
||||
if (!group || !accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='members'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />}
|
||||
>
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
|
||||
import {
|
||||
fetchRemovedAccounts,
|
||||
expandRemovedAccounts,
|
||||
removeRemovedAccount,
|
||||
} from '../../../actions/groups';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Column from '../../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'groups.removed_accounts', defaultMessage: 'Allow joining' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.user_lists.groups_removed_accounts.get(id)?.items,
|
||||
hasMore: !!state.user_lists.groups_removed_accounts.get(id)?.next,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class GroupRemovedAccounts extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchRemovedAccounts(id));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.params.id !== prevProps.params.id) {
|
||||
this.props.dispatch(fetchRemovedAccounts(this.props.params.id));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandRemovedAccounts(this.props.params.id));
|
||||
}, 300, { leading: true });
|
||||
|
||||
handleOnActionClick = (group, id) => {
|
||||
return () => {
|
||||
this.props.dispatch(removeRemovedAccount(group.get('id'), id));
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { accountIds, hasMore, group, intl } = this.props;
|
||||
|
||||
if (!group || !accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='removed_accounts'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='group.removed_accounts.empty' defaultMessage='This group does not has any removed accounts.' />}
|
||||
>
|
||||
{accountIds.map(id => (<AccountContainer
|
||||
key={id}
|
||||
id={id}
|
||||
actionIcon={require('@tabler/icons/x.svg')}
|
||||
onActionClick={this.handleOnActionClick(group, id)}
|
||||
actionTitle={intl.formatMessage(messages.remove)}
|
||||
/>))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
import Item from './item';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'groups.sidebar-panel.title', defaultMessage: 'Groups You\'re In' },
|
||||
show_all: { id: 'groups.sidebar-panel.show_all', defaultMessage: 'Show all' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
groupIds: state.getIn(['group_lists', 'member']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class GroupSidebarPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
groupIds: ImmutablePropTypes.list,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, groupIds } = this.props;
|
||||
const count = groupIds.count();
|
||||
|
||||
// Only when there are groups to show
|
||||
if (count === 0) return null;
|
||||
|
||||
return (
|
||||
<div className='wtf-panel group-sidebar-panel'>
|
||||
<div className='wtf-panel-header'>
|
||||
<Icon id='users' className='wtf-panel-header__icon' />
|
||||
<span className='wtf-panel-header__label'>{intl.formatMessage(messages.title)}</span>
|
||||
</div>
|
||||
|
||||
<div className='wtf-panel__content'>
|
||||
<div className='group-sidebar-panel__items'>
|
||||
{groupIds.slice(0, 10).map(groupId => <Item key={groupId} id={groupId} />)}
|
||||
{count > 10 && <Link className='group-sidebar-panel__items__show-all' to='/groups/browse/member'>{intl.formatMessage(messages.show_all)}</Link>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { shortNumberFormat } from '../../../utils/numbers';
|
||||
|
||||
const messages = defineMessages({
|
||||
new_statuses: { id: 'groups.sidebar-panel.item.view', defaultMessage: 'new posts' },
|
||||
no_recent_activity: { id: 'groups.sidebar-panel.item.no_recent_activity', defaultMessage: 'No recent activity' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
relationships: state.getIn(['group_relationships', id]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Item extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, group, relationships } = this.props;
|
||||
|
||||
// Wait for relationships
|
||||
if (!relationships) return null;
|
||||
|
||||
const unreadCount = relationships.get('unread_count');
|
||||
|
||||
return (
|
||||
<Link to={`/groups/${group.get('id')}`} className='group-sidebar-panel__item'>
|
||||
<div className='group-sidebar-panel__item__title'>{group.get('title')}</div>
|
||||
<div className='group-sidebar-panel__item__meta'>
|
||||
{unreadCount > 0 && <span className='group-sidebar-panel__item__meta__unread'>{shortNumberFormat(unreadCount)} {intl.formatMessage(messages.new_statuses)}</span>}
|
||||
{unreadCount === 0 && <span>{intl.formatMessage(messages.no_recent_activity)}</span>}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,92 +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 { defineMessages, injectIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
|
||||
import DropdownMenuContainer from '../../../../containers/dropdown_menu_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
join: { id: 'groups.join', defaultMessage: 'Join group' },
|
||||
leave: { id: 'groups.leave', defaultMessage: 'Leave group' },
|
||||
removed_accounts: { id: 'groups.removed_accounts', defaultMessage: 'Removed Accounts' },
|
||||
edit: { id: 'groups.edit', defaultMessage: 'Edit' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
toggleMembership: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
toggle = () => {
|
||||
const { group, relationships, toggleMembership } = this.props;
|
||||
toggleMembership(group, relationships);
|
||||
}
|
||||
|
||||
getActionButton() {
|
||||
const { group, relationships, intl } = this.props;
|
||||
|
||||
if (!relationships) {
|
||||
return '';
|
||||
} else if (!relationships.get('member')) {
|
||||
return <Button className='logo-button' text={intl.formatMessage(messages.join)} onClick={this.toggle} />;
|
||||
} else if (relationships.get('member')) {
|
||||
return <Button className='logo-button' text={intl.formatMessage(messages.leave, { name: group.get('title') })} onClick={this.toggle} />;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
getAdminMenu() {
|
||||
const { group, intl } = this.props;
|
||||
|
||||
const menu = [
|
||||
{
|
||||
text: intl.formatMessage(messages.edit),
|
||||
to: `/groups/${group.get('id')}/edit`,
|
||||
icon: require('@tabler/icons/edit.svg'),
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.removed_accounts),
|
||||
to: `/groups/${group.get('id')}/removed_accounts`,
|
||||
icon: require('@tabler/icons/trash.svg'),
|
||||
destructive: true,
|
||||
},
|
||||
];
|
||||
|
||||
return <DropdownMenuContainer items={menu} src={require('@tabler/icons/dots-vertical.svg')} direction='right' />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { group, relationships } = this.props;
|
||||
|
||||
if (!group || !relationships) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='group__header-container'>
|
||||
<div className='group__header'>
|
||||
<div className='group__cover'>
|
||||
<img src={group.get('cover_image_url')} alt='' className='parallax' />
|
||||
</div>
|
||||
|
||||
<div className='group__tabs'>
|
||||
<NavLink exact className='group__tabs__tab' activeClassName='group__tabs__tab--active' to={`/groups/${group.get('id')}`}>Posts</NavLink>
|
||||
<NavLink exact className='group__tabs__tab' activeClassName='group__tabs__tab--active' to={`/groups/${group.get('id')}/members`}>Members</NavLink>
|
||||
{this.getActionButton()}
|
||||
{relationships.get('admin') && this.getAdminMenu()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
group_archived: { id: 'group.detail.archived_group', defaultMessage: 'Archived group' },
|
||||
group_admin: { id: 'groups.detail.role_admin', defaultMessage: 'You\'re an admin' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class GroupPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { group, relationships, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='group__panel'>
|
||||
<h1 className='group__panel__title'>
|
||||
{group.get('title')}
|
||||
{group.get('archived') && <Icon id='lock' title={intl.formatMessage(messages.group_archived)} />}
|
||||
</h1>
|
||||
|
||||
{relationships.get('admin') && <span className='group__panel__label'>{intl.formatMessage(messages.group_admin)}</span>}
|
||||
|
||||
<div className='group__panel__description'>{group.get('description')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { joinGroup, leaveGroup } from '../../../../actions/groups';
|
||||
import Header from '../components/header';
|
||||
|
||||
const mapStateToProps = (state, { groupId }) => ({
|
||||
group: state.getIn(['groups', groupId]),
|
||||
relationships: state.getIn(['group_relationships', groupId]),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
toggleMembership(group, relationships) {
|
||||
if (relationships.get('member')) {
|
||||
dispatch(leaveGroup(group.get('id')));
|
||||
} else {
|
||||
dispatch(joinGroup(group.get('id')));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Header);
|
|
@ -1,107 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
|
||||
import ComposeFormContainer from '../../../../soapbox/features/compose/containers/compose_form_container';
|
||||
import { connectGroupStream } from '../../../actions/streaming';
|
||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import MissingIndicator from '../../../components/missing_indicator';
|
||||
import Timeline from '../../ui/components/timeline';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const me = state.get('me');
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
relationships: state.getIn(['group_relationships', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class GroupTimeline extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
relationships: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.record,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(expandGroupTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectGroupStream(id));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { columnId, group, relationships, account } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
if (typeof group === 'undefined' || !relationships) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
} else if (group === false) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
const acct = account ? account.get('acct') : '';
|
||||
|
||||
return (
|
||||
<div>
|
||||
{relationships.get('member') && (
|
||||
<div className='timeline-compose-block'>
|
||||
<Link className='timeline-compose-block__avatar' to={`/@${acct}`}>
|
||||
<Avatar account={account} size={46} />
|
||||
</Link>
|
||||
<ComposeFormContainer group={group} shouldCondense autoFocus={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='group__feed'>
|
||||
<Timeline
|
||||
alwaysPrepend
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
timelineId={`group:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
group={group}
|
||||
withGroupAdmin={relationships && relationships.get('admin')}
|
||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group make new posts, they will appear here.' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
import classNames from 'clsx';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import { closeOnboarding } from '../../actions/onboarding';
|
||||
|
||||
const FrameWelcome = ({ domain, onNext }) => (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--centered'>
|
||||
<h3><FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' /></h3>
|
||||
<p><FormattedMessage id='introduction.welcome.text' defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name." values={{ domain: <code>{domain}</code> }} /></p>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
FrameWelcome.propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const FrameFederation = ({ onNext }) => (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--columnized'>
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' /></h3>
|
||||
<p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.federation.action' defaultMessage='Next' /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
FrameFederation.propTypes = {
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const FrameInteractions = ({ onNext }) => (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--columnized'>
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' /></h3>
|
||||
<p><FormattedMessage id='introduction.interactions.reply.text' defaultMessage="You can reply to other people's and your own posts, which will chain them together in a conversation." /></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Repost' /></h3>
|
||||
<p><FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's posts with your followers by reposting them." /></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favorite' /></h3>
|
||||
<p><FormattedMessage id='introduction.interactions.favourite.text' defaultMessage='You can save a post for later, and let the author know that you liked it, by favoriting it.' /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
FrameInteractions.propTypes = {
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default @connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
|
||||
class Introduction extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
currentIndex: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.pages = [
|
||||
<FrameWelcome domain={props.domain} onNext={this.handleNext} />,
|
||||
<FrameFederation onNext={this.handleNext} />,
|
||||
<FrameInteractions onNext={this.handleFinish} />,
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
handleDot = (e) => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
this.setState({ currentIndex: i });
|
||||
}
|
||||
|
||||
handlePrev = () => {
|
||||
this.setState(({ currentIndex }) => ({
|
||||
currentIndex: Math.max(0, currentIndex - 1),
|
||||
}));
|
||||
}
|
||||
|
||||
handleNext = () => {
|
||||
const { pages } = this;
|
||||
|
||||
this.setState(({ currentIndex }) => ({
|
||||
currentIndex: Math.min(currentIndex + 1, pages.length - 1),
|
||||
}));
|
||||
}
|
||||
|
||||
handleSwipe = (index) => {
|
||||
this.setState({ currentIndex: index });
|
||||
}
|
||||
|
||||
handleFinish = () => {
|
||||
this.props.dispatch(closeOnboarding());
|
||||
}
|
||||
|
||||
handleKeyUp = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
this.handlePrev();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.handleNext();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentIndex } = this.state;
|
||||
const { pages } = this;
|
||||
|
||||
return (
|
||||
<div className='introduction'>
|
||||
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='introduction__pager'>
|
||||
{pages.map((page, i) => (
|
||||
<div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
|
||||
))}
|
||||
</ReactSwipeableViews>
|
||||
|
||||
<div className='introduction__dots'>
|
||||
{pages.map((_, i) => (
|
||||
<div
|
||||
key={`dot-${i}`}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
data-index={i}
|
||||
onClick={this.handleDot}
|
||||
className={classNames('introduction__dot', { active: i === currentIndex })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -38,30 +38,6 @@ export function ListTimeline() {
|
|||
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
|
||||
}
|
||||
|
||||
export function GroupTimeline() {
|
||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/timeline');
|
||||
}
|
||||
|
||||
export function GroupMembers() {
|
||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/members');
|
||||
}
|
||||
|
||||
export function GroupRemovedAccounts() {
|
||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/removed_accounts');
|
||||
}
|
||||
|
||||
export function GroupCreate() {
|
||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/create');
|
||||
}
|
||||
|
||||
export function GroupEdit() {
|
||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/edit');
|
||||
}
|
||||
|
||||
export function Groups() {
|
||||
return import(/* webpackChunkName: "features/groups/index" */'../../groups/index');
|
||||
}
|
||||
|
||||
export function Lists() {
|
||||
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
// Wrapper to call requestIdleCallback() to schedule low-priority work.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Background_Tasks_API
|
||||
// for a good breakdown of the concepts behind this.
|
||||
|
||||
import Queue from 'tiny-queue';
|
||||
|
||||
const taskQueue = new Queue();
|
||||
let runningRequestIdleCallback = false;
|
||||
|
||||
function runTasks(deadline) {
|
||||
while (taskQueue.length && deadline.timeRemaining() > 0) {
|
||||
taskQueue.shift()();
|
||||
}
|
||||
if (taskQueue.length) {
|
||||
requestIdleCallback(runTasks);
|
||||
} else {
|
||||
runningRequestIdleCallback = false;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleIdleTask(task) {
|
||||
taskQueue.push(task);
|
||||
if (!runningRequestIdleCallback) {
|
||||
runningRequestIdleCallback = true;
|
||||
requestIdleCallback(runTasks);
|
||||
}
|
||||
}
|
||||
|
||||
export default scheduleIdleTask;
|
|
@ -497,7 +497,7 @@ class Video extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, aspectRatio, blurhash } = this.props;
|
||||
const { src, inline, onCloseVideo, intl, alt, detailed, sensitive, link, aspectRatio, blurhash } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = (currentTime / duration) * 100;
|
||||
const playerStyle = {};
|
||||
|
@ -614,8 +614,6 @@ class Video extends React.PureComponent {
|
|||
|
||||
<div className='video-player__buttons right'>
|
||||
{(sensitive && !onCloseVideo) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon src={require('@tabler/icons/eye-off.svg')} /></button>}
|
||||
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} className='player-button' onClick={this.handleOpenVideo}><Icon src={require('@tabler/icons/maximize.svg')} /></button>}
|
||||
{/* onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} className='player-button' onClick={this.handleCloseVideo}><Icon src={require('@tabler/icons/x.svg')} /></button> */}
|
||||
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} className='player-button' onClick={this.toggleFullscreen}><Icon src={fullscreen ? require('@tabler/icons/arrows-minimize.svg') : require('@tabler/icons/arrows-maximize.svg')} /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,75 +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 { connect } from 'react-redux';
|
||||
|
||||
import { fetchGroup } from '../actions/groups';
|
||||
import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||
import GroupPanel from '../features/groups/timeline/components/panel';
|
||||
import HeaderContainer from '../features/groups/timeline/containers/header_container';
|
||||
import LinkFooter from '../features/ui/components/link_footer';
|
||||
import PromoPanel from '../features/ui/components/promo_panel';
|
||||
import WhoToFollowPanel from '../features/ui/components/who-to-follow-panel';
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
relationships: state.getIn(['group_relationships', id]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class GroupPage extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { params: { id }, dispatch } = this.props;
|
||||
|
||||
dispatch(fetchGroup(id));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, group, relationships } = this.props;
|
||||
|
||||
return (
|
||||
<div className='page group'>
|
||||
{group && <HeaderContainer groupId={group.get('id')} />}
|
||||
|
||||
<div className='page__columns'>
|
||||
<div className='columns-area__panels'>
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
{group && relationships &&
|
||||
<GroupPanel
|
||||
group={group}
|
||||
relationships={relationships}
|
||||
/>}
|
||||
|
||||
<PromoPanel />
|
||||
<LinkFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__main'>
|
||||
<div className='columns-area'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<GroupSidebarPanel />
|
||||
<WhoToFollowPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||
import LinkFooter from '../features/ui/components/link_footer';
|
||||
import PromoPanel from '../features/ui/components/promo_panel';
|
||||
import UserPanel from '../features/ui/components/user_panel';
|
||||
import WhoToFollowPanel from '../features/ui/components/who-to-follow-panel';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class GroupsPage extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<div className='page'>
|
||||
<div className='page__columns'>
|
||||
<div className='columns-area__panels'>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<UserPanel />
|
||||
<PromoPanel />
|
||||
<LinkFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__main'>
|
||||
<div className='columns-area'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<GroupSidebarPanel />
|
||||
<WhoToFollowPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@ import {
|
|||
SEARCH_EXPAND_REQUEST,
|
||||
SEARCH_EXPAND_SUCCESS,
|
||||
SEARCH_ACCOUNT_SET,
|
||||
SEARCH_RESULTS_CLEAR,
|
||||
} from '../actions/search';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
@ -82,7 +83,7 @@ const paginateResults = (state: State, searchType: SearchFilter, results: APIEnt
|
|||
const data = results[searchType];
|
||||
// Hashtags are a list of maps. Others are IDs.
|
||||
if (searchType === 'hashtags') {
|
||||
return (items as ImmutableOrderedSet<string>).concat(fromJS(data));
|
||||
return (items as ImmutableOrderedSet<string>).concat((fromJS(data) as Record<string, any>).map(normalizeTag));
|
||||
} else {
|
||||
return (items as ImmutableOrderedSet<string>).concat(toIds(data));
|
||||
}
|
||||
|
@ -105,6 +106,13 @@ export default function search(state = ReducerRecord(), action: AnyAction) {
|
|||
return state.set('value', action.value);
|
||||
case SEARCH_CLEAR:
|
||||
return ReducerRecord();
|
||||
case SEARCH_RESULTS_CLEAR:
|
||||
return state.merge({
|
||||
value: '',
|
||||
results: ResultsRecord(),
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
});
|
||||
case SEARCH_SHOW:
|
||||
return state.set('hidden', false);
|
||||
case COMPOSE_REPLY:
|
||||
|
@ -123,7 +131,13 @@ export default function search(state = ReducerRecord(), action: AnyAction) {
|
|||
case SEARCH_EXPAND_SUCCESS:
|
||||
return paginateResults(state, action.searchType, action.results, action.searchTerm);
|
||||
case SEARCH_ACCOUNT_SET:
|
||||
if (!action.accountId) return state.set('accountId', null);
|
||||
if (!action.accountId) return state.merge({
|
||||
results: ResultsRecord(),
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
filter: 'accounts',
|
||||
accountId: null,
|
||||
});
|
||||
return ReducerRecord({ accountId: action.accountId, filter: 'statuses' });
|
||||
default:
|
||||
return state;
|
||||
|
|
Loading…
Reference in New Issue