New groups
This commit is contained in:
parent
fd50f03304
commit
1fabd28498
|
@ -9,7 +9,7 @@ class Api::V1::Groups::AccountsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
before_action :set_group
|
||||
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
after_action :insert_pagination_headers, only: :show
|
||||
|
||||
def show
|
||||
@accounts = load_accounts
|
||||
|
|
|
@ -10,10 +10,24 @@ class Api::V1::GroupsController < Api::BaseController
|
|||
before_action :set_group, except: [:index, :create]
|
||||
|
||||
def index
|
||||
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).all
|
||||
case current_tab
|
||||
when 'featured'
|
||||
@groups = Group.where(is_featured: true).limit(25).all
|
||||
when 'member'
|
||||
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).all
|
||||
when 'admin'
|
||||
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account, write_permissions: true }).all
|
||||
end
|
||||
|
||||
render json: @groups, each_serializer: REST::GroupSerializer
|
||||
end
|
||||
|
||||
def current_tab
|
||||
tab = 'featured'
|
||||
tab = params[:tab] if ['featured', 'member', 'admin'].include? params[:tab]
|
||||
return tab
|
||||
end
|
||||
|
||||
def show
|
||||
render json: @group, serializer: REST::GroupSerializer
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||
end
|
||||
|
||||
def group_statuses
|
||||
statuses = tag_timeline_statuses.paginate_by_id(
|
||||
statuses = group_timeline_statuses.paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
|
@ -43,7 +43,7 @@ class Api::V1::Timelines::GroupController < Api::BaseController
|
|||
end
|
||||
end
|
||||
|
||||
def group_statuses
|
||||
def group_timeline_statuses
|
||||
GroupQueryService.new.call(@group)
|
||||
end
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ export function directCompose(account, routerHistory) {
|
|||
};
|
||||
};
|
||||
|
||||
export function submitCompose(routerHistory) {
|
||||
export function submitCompose(routerHistory, group) {
|
||||
return function (dispatch, getState) {
|
||||
if (!me) return;
|
||||
|
||||
|
@ -147,6 +147,7 @@ export function submitCompose(routerHistory) {
|
|||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
group_id: group ? group.get('id') : null,
|
||||
}, {
|
||||
headers: {
|
||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import api from '../api';
|
||||
import api, { getLinks } from '../api';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
||||
export const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST';
|
||||
export const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS';
|
||||
|
@ -21,6 +23,14 @@ export const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST';
|
|||
export const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS';
|
||||
export const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL';
|
||||
|
||||
export const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST';
|
||||
export const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS';
|
||||
export const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL';
|
||||
|
||||
export const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST';
|
||||
export const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS';
|
||||
export const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL';
|
||||
|
||||
export const fetchGroup = id => (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
|
@ -98,13 +108,16 @@ export function fetchGroupRelationshipsFail(error) {
|
|||
};
|
||||
};
|
||||
|
||||
export const fetchGroups = () => (dispatch, getState) => {
|
||||
export const fetchGroups = (tab) => (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
dispatch(fetchGroupsRequest());
|
||||
|
||||
api(getState).get('/api/v1/groups')
|
||||
.then(({ data }) => dispatch(fetchGroupsSuccess(data)))
|
||||
api(getState).get('/api/v1/groups?tab=' + tab)
|
||||
.then(({ data }) => {
|
||||
dispatch(fetchGroupsSuccess(data, tab));
|
||||
dispatch(fetchGroupRelationships(data.map(item => item.id)));
|
||||
})
|
||||
.catch(err => dispatch(fetchGroupsFail(err)));
|
||||
};
|
||||
|
||||
|
@ -112,9 +125,10 @@ export const fetchGroupsRequest = () => ({
|
|||
type: GROUPS_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchGroupsSuccess = groups => ({
|
||||
export const fetchGroupsSuccess = (groups, tab) => ({
|
||||
type: GROUPS_FETCH_SUCCESS,
|
||||
groups,
|
||||
tab,
|
||||
});
|
||||
|
||||
export const fetchGroupsFail = error => ({
|
||||
|
@ -191,3 +205,93 @@ export function leaveGroupFail(error) {
|
|||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMembers(id) {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
dispatch(fetchMembersRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchMembersFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMembersRequest(id) {
|
||||
return {
|
||||
type: GROUP_MEMBERS_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMembersSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: GROUP_MEMBERS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchMembersFail(id, error) {
|
||||
return {
|
||||
type: GROUP_MEMBERS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMembers(id) {
|
||||
return (dispatch, getState) => {
|
||||
if (!me) return;
|
||||
|
||||
const url = getState().getIn(['user_lists', 'groups', id, 'next']);
|
||||
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandMembersRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(expandMembersFail(id, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMembersRequest(id) {
|
||||
return {
|
||||
type: GROUP_MEMBERS_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMembersSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandMembersFail(id, error) {
|
||||
return {
|
||||
type: GROUP_MEMBERS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -68,6 +68,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
anyMedia: PropTypes.bool,
|
||||
shouldCondense: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
group: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -118,7 +119,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(this.context.router ? this.context.router.history : null);
|
||||
this.props.onSubmit(this.context.router ? this.context.router.history : null, this.props.group);
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
|
|
|
@ -33,8 +33,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
dispatch(changeCompose(text));
|
||||
},
|
||||
|
||||
onSubmit (router) {
|
||||
dispatch(submitCompose(router));
|
||||
onSubmit (router, group) {
|
||||
dispatch(submitCompose(router, group));
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { shortNumberFormat } from '../../../utils/numbers';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
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) return null;
|
||||
if (relationships.get('admin')) return intl.formatMessage(messages.role_admin);
|
||||
if (relationships.get('member')) return intl.formatMessage(messages.role_member);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,80 +2,75 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import Column from '../../ui/components/column';
|
||||
import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
|
||||
import { fetchGroups } from '../../../actions/groups';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ColumnLink from '../../ui/components/column_link';
|
||||
import ColumnSubheading from '../../ui/components/column_subheading';
|
||||
import NewGroupForm from '../create';
|
||||
import { createSelector } from 'reselect';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import GroupCard from './card';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||
subheading: { id: 'groups.subheading', defaultMessage: 'Your groups' },
|
||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||
tab_featured: { id: 'column.groups_tab_featured', defaultMessage: 'Featured' },
|
||||
tab_member: { id: 'column.groups_tab_member', defaultMessage: 'Groups you\'re in' },
|
||||
tab_admin: { id: 'column.groups_tab_admin', defaultMessage: 'Groups you manage' },
|
||||
});
|
||||
|
||||
const getOrderedGroups = createSelector([state => state.get('groups')], groups => {
|
||||
if (!groups) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
return groups.toList().filter(item => !!item);
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
groups: getOrderedGroups(state),
|
||||
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,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
groups: ImmutablePropTypes.map,
|
||||
groupIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
groups: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchGroups());
|
||||
}
|
||||
componentDidUpdate(oldProps) {
|
||||
if (this.props.activeTab && this.props.activeTab !== oldProps.activeTab) {
|
||||
this.props.dispatch(fetchGroups(this.props.activeTab));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, groups } = this.props;
|
||||
|
||||
if (!groups) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.groups' defaultMessage="No groups." />;
|
||||
|
||||
return (
|
||||
<Column icon='list-ul' heading={intl.formatMessage(messages.heading)}>
|
||||
<ColumnBackButtonSlim />
|
||||
|
||||
<NewGroupForm />
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{groups.map(group =>
|
||||
<ColumnLink key={group.get('id')} to={`/groups/${group.get('id')}`} icon='list-ul' text={group.get('title')} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
render () {
|
||||
const { intl, groupIds, activeTab } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="group-column-header">
|
||||
<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>
|
||||
|
||||
<div className="group-card-list">
|
||||
{groupIds.map(id => <GroupCard key={id} id={id} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import {
|
||||
fetchMembers,
|
||||
expandMembers,
|
||||
} from '../../../actions/groups';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Column from '../../ui/components/column';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class GroupMembers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchMembers(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchMembers(nextProps.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>
|
||||
<LoadingIndicator />
|
||||
</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,44 +1,62 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import InnerHeader from './inner_header';
|
||||
import Button from 'gabsocial/components/button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export default class Header extends ImmutablePureComponent {
|
||||
const messages = defineMessages({
|
||||
join: { id: 'groups.join', defaultMessage: 'Join group' },
|
||||
leave: { id: 'groups.leave', defaultMessage: 'Leave group' },
|
||||
});
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
toggleMembership: PropTypes.func.isRequired,
|
||||
};
|
||||
export default @injectIntl
|
||||
class Header extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
toggleMembership: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { group, relationships, toggleMembership } = this.props;
|
||||
getActionButton() {
|
||||
const { group, relationships, toggleMembership, intl } = this.props;
|
||||
const toggle = () => toggleMembership(group, relationships);
|
||||
|
||||
if (group === null) {
|
||||
return null;
|
||||
}
|
||||
if (!relationships) {
|
||||
return '';
|
||||
} else if (!relationships.get('member')) {
|
||||
return <Button className='logo-button' text={intl.formatMessage(messages.join)} onClick={toggle} />;
|
||||
} else if (relationships.get('member')) {
|
||||
return <Button className='logo-button' text={intl.formatMessage(messages.leave, { name: group.get('title') })} onClick={toggle} />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account-timeline__header'>
|
||||
<InnerHeader
|
||||
group={group}
|
||||
relationships={relationships}
|
||||
toggleMembership={toggleMembership}
|
||||
/>
|
||||
render () {
|
||||
const { group, relationships } = this.props;
|
||||
|
||||
<div className='account__section-headline'>
|
||||
<NavLink exact to={`/groups/${group.get('id')}`}><FormattedMessage id='groups.posts' defaultMessage='Posts' /></NavLink>
|
||||
<NavLink exact to={`/groups/${group.get('id')}/accounts`}><FormattedMessage id='group.accounts' defaultMessage='Members' /></NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Button from 'gabsocial/components/button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import DropdownMenuContainer from 'gabsocial/containers/dropdown_menu_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
join: { id: 'groups.join', defaultMessage: 'Join' },
|
||||
leave: { id: 'groups.leave', defaultMessage: 'Leave' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class InnerHeader extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
relationships: ImmutablePropTypes.map,
|
||||
toggleMembership: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
isStatusesPageActive = (match, location) => {
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !location.pathname.match(/\/(accounts)\/?$/);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { group, relationships, intl } = this.props;
|
||||
|
||||
if (!group || !relationships) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let info = [];
|
||||
let actionBtn = '';
|
||||
let lockedIcon = '';
|
||||
let menu = [];
|
||||
|
||||
if (relationships.get('admin')) {
|
||||
info.push(<span key='admin'><FormattedMessage id='group.admin' defaultMessage='You are an admin' /></span>);
|
||||
}
|
||||
|
||||
if (!relationships) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (!relationships.get('member')) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.join)} onClick={() => this.props.toggleMembership(group, relationships)} />;
|
||||
} else if (relationships.get('member')) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.leave, { name: group.get('title') })} onClick={() => this.props.toggleMembership(group, relationships)} />;
|
||||
}
|
||||
|
||||
if (group.get('archived')) {
|
||||
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.group_archived)} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account__header'>
|
||||
<div className='account__header__image'>
|
||||
<div className='account__header__info'>
|
||||
<img src={group.get('cover_image_url')} alt='' className='parallax' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__bar'>
|
||||
<div className='account__header__tabs'>
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{actionBtn}
|
||||
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
<h1>
|
||||
<span>{group.get('title')} {info}</span>
|
||||
<small>{lockedIcon}</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{group.get('description').length > 0 && <div className='account__header__content'>{group.get('description')}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,102 +4,98 @@ import PropTypes from 'prop-types';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||
import Column from '../../../components/column';
|
||||
import ColumnBackButton from '../../../components/column_back_button';
|
||||
import ColumnHeader from '../../../components/column_header';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { connectGroupStream } from '../../../actions/streaming';
|
||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||
import { fetchGroup } from '../../../actions/groups';
|
||||
import MissingIndicator from '../../../components/missing_indicator';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import HeaderContainer from './containers/header_container';
|
||||
import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
import Avatar from '../../../components/avatar';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
|
||||
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 contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
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.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(expandGroupTimeline(id));
|
||||
|
||||
dispatch(fetchGroup(id));
|
||||
dispatch(expandGroupTimeline(id));
|
||||
this.disconnect = dispatch(connectGroupStream(id));
|
||||
}
|
||||
|
||||
this.disconnect = dispatch(connectGroupStream(id));
|
||||
}
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
handleLoadMore = maxId => {
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, group } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const title = group ? group.get('title') : id;
|
||||
if (typeof group === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
} else if (group === false) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof group === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<div>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
} else if (group === false) {
|
||||
return (
|
||||
<Column>
|
||||
<ColumnBackButton />
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{relationships && relationships.get('member') && (
|
||||
<div className='timeline-compose-block'>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Avatar account={account} size={46} />
|
||||
</div>
|
||||
<ComposeFormContainer group={group} shouldCondense={true} autoFocus={false}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
return (
|
||||
<Column label={title}>
|
||||
<ColumnHeader icon='list-ul' active={hasUnread} title={title}>
|
||||
<div className='column-header__links'>
|
||||
{/* Leave might be here */}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
prepend={<HeaderContainer groupId={id} />}
|
||||
alwaysPrepend
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
timelineId={`group:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
<StatusListContainer
|
||||
alwaysPrepend
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
timelineId={`group:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,9 @@ export const privateLinks = [
|
|||
<NotificationsCounterIcon />
|
||||
<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />
|
||||
</NavLink>,
|
||||
<NavLink className='tabs-bar__link groups' to='/groups' data-preview-title-id='column.groups' >
|
||||
<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
||||
</NavLink>,
|
||||
// <NavLink className='tabs-bar__link home' to='/groups' data-preview-title-id='column.groups' >
|
||||
// <FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
|
||||
// </NavLink>,
|
||||
|
|
|
@ -25,6 +25,8 @@ import TabsBar from './components/tabs_bar';
|
|||
import WhoToFollowPanel from './components/who_to_follow_panel';
|
||||
import LinkFooter from './components/link_footer';
|
||||
import ProfilePage from 'gabsocial/pages/profile_page';
|
||||
import GroupsPage from 'gabsocial/pages/groups_page';
|
||||
import GroupPage from 'gabsocial/pages/group_page';
|
||||
import SearchPage from 'gabsocial/pages/search_page';
|
||||
import HomePage from 'gabsocial/pages/home_page';
|
||||
|
||||
|
@ -54,6 +56,7 @@ import {
|
|||
Explore,
|
||||
Groups,
|
||||
GroupTimeline,
|
||||
GroupMembers,
|
||||
} from './util/async-components';
|
||||
import { me, meUsername } from '../../initial_state';
|
||||
import { previewState as previewMediaState } from './components/media_modal';
|
||||
|
@ -170,8 +173,11 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/home' exact page={HomePage} component={HomeTimeline} content={children} />
|
||||
<WrappedRoute path='/timeline/all' exact page={HomePage} component={CommunityTimeline} content={children} />
|
||||
|
||||
<WrappedRoute path='/groups' component={Groups} content={children} />
|
||||
<WrappedRoute path='/groups/:id' component={GroupTimeline} content={children} />
|
||||
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
|
||||
<WrappedRoute path='/groups/browse/member' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'member' }} />
|
||||
<WrappedRoute path='/groups/browse/admin' page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'admin' }} />
|
||||
<WrappedRoute path='/groups/:id/members' page={GroupPage} component={GroupMembers} content={children} />
|
||||
<WrappedRoute path='/groups/:id' page={GroupPage} component={GroupTimeline} content={children} />
|
||||
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
|
||||
|
|
|
@ -38,6 +38,10 @@ export function GroupTimeline () {
|
|||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/timeline');
|
||||
}
|
||||
|
||||
export function GroupMembers () {
|
||||
return import(/* webpackChunkName: "features/groups/timeline" */'../../groups/members');
|
||||
}
|
||||
|
||||
export function Groups () {
|
||||
return import(/* webpackChunkName: "features/groups/index" */'../../groups/index');
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"column.lists": "Lists",
|
||||
"column.mutes": "Muted users",
|
||||
"column.notifications": "Notifications",
|
||||
"column.groups": "Groups",
|
||||
"column.pins": "Pinned gabs",
|
||||
"column.public": "Federated timeline",
|
||||
"column_back_button.label": "Back",
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { PropTypes } from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import WhoToFollowPanel from '../features/ui/components/who_to_follow_panel';
|
||||
import LinkFooter from '../features/ui/components/link_footer';
|
||||
import PromoPanel from '../features/ui/components/promo_panel';
|
||||
import HeaderContainer from '../features/groups/timeline/containers/header_container';
|
||||
import GroupPanel from '../features/groups/timeline/components/panel';
|
||||
import { fetchGroup } from '../actions/groups';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
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 columns-area--mobile'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<WhoToFollowPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import WhoToFollowPanel from '../features/ui/components/who_to_follow_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';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
account: state.getIn(['accounts', me]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class GroupsPage extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map
|
||||
};
|
||||
|
||||
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 columns-area--mobile'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<WhoToFollowPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { GROUPS_FETCH_SUCCESS } from '../actions/groups';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
featured: ImmutableList(),
|
||||
member: ImmutableList(),
|
||||
admin: ImmutableList(),
|
||||
});
|
||||
|
||||
const normalizeList = (state, type, id, groups) => {
|
||||
return state.set(type, ImmutableList(groups.map(item => item.id)));
|
||||
};
|
||||
|
||||
export default function groupLists(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case GROUPS_FETCH_SUCCESS:
|
||||
return normalizeList(state, action.tab, action.id, action.groups);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -34,6 +34,7 @@ import identity_proofs from './identity_proofs';
|
|||
import trends from './trends';
|
||||
import groups from './groups';
|
||||
import group_relationships from './group_relationships';
|
||||
import group_lists from './group_lists';
|
||||
|
||||
const reducers = {
|
||||
dropdown_menu,
|
||||
|
@ -71,6 +72,7 @@ const reducers = {
|
|||
trends,
|
||||
groups,
|
||||
group_relationships,
|
||||
group_lists,
|
||||
};
|
||||
|
||||
export default combineReducers(reducers);
|
||||
|
|
|
@ -21,6 +21,10 @@ import {
|
|||
MUTES_EXPAND_SUCCESS,
|
||||
} from '../actions/mutes';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import {
|
||||
GROUP_MEMBERS_FETCH_SUCCESS,
|
||||
GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||
} from '../actions/groups';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
followers: ImmutableMap(),
|
||||
|
@ -30,6 +34,7 @@ const initialState = ImmutableMap({
|
|||
follow_requests: ImmutableMap(),
|
||||
blocks: ImmutableMap(),
|
||||
mutes: ImmutableMap(),
|
||||
groups: ImmutableMap(),
|
||||
});
|
||||
|
||||
const normalizeList = (state, type, id, accounts, next) => {
|
||||
|
@ -74,6 +79,10 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case GROUP_MEMBERS_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'groups', action.id, action.accounts, action.next);
|
||||
case GROUP_MEMBERS_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'groups', action.id, action.accounts, action.next);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
@import 'gabsocial/components/account-header';
|
||||
@import 'gabsocial/components/user-panel';
|
||||
@import 'gabsocial/components/compose-form';
|
||||
@import 'gabsocial/components/group-card';
|
||||
@import 'gabsocial/components/group-detail';
|
||||
|
||||
@import 'gabsocial/polls';
|
||||
@import 'gabsocial/introduction';
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
$height: 80px;
|
||||
|
||||
.group-column-header {
|
||||
border-radius: 10px;
|
||||
background: $gab-background-container;
|
||||
overflow: hidden;
|
||||
|
||||
.group-column-header__title {
|
||||
padding: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.group-card-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
display: block;
|
||||
flex: 0 0 calc(50% - 15px/2);
|
||||
margin-bottom: 15px;
|
||||
background: $gab-background-container;
|
||||
text-decoration: none;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.group-card__header {
|
||||
height: $height;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
background: $gab-background-container;;
|
||||
}
|
||||
}
|
||||
|
||||
.group-card__content {
|
||||
padding: 15px;
|
||||
|
||||
.group-card__title {
|
||||
color: $primary-text-color;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.group-card__meta {
|
||||
color: $gab-secondary-text;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.group-card__description {
|
||||
color: $primary-text-color;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.group-card__title {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
.group {
|
||||
.group__header-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.group__header {
|
||||
width: 100%;
|
||||
max-width: 1150px;
|
||||
background: $gab-background-container;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 20px 0;
|
||||
|
||||
.group__cover {
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.group__tabs {
|
||||
.group__tabs__tab {
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
padding: 16px 22px;
|
||||
text-align: center;
|
||||
color: $primary-text-color;
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
border-bottom: 2px solid $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
clear: both;
|
||||
display: table;
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
margin: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group__panel {
|
||||
padding: 10px 10px 20px 10px;
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.group__panel__description {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.group__panel__label {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
background: $gab-background-container;
|
||||
font-size: 13px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -147,6 +147,7 @@
|
|||
&.explore,
|
||||
&.notifications,
|
||||
&.messages,
|
||||
&.groups,
|
||||
&.optional {
|
||||
padding: 16px 0 0 23px;
|
||||
background-size: 14px 114px;
|
||||
|
@ -161,6 +162,7 @@
|
|||
&.notifications {background-image: url('../images/gab-icons/icon-notifications-sprite.svg');}
|
||||
&.messages {background-image: url('../images/gab-icons/icon-messages-sprite.svg');}
|
||||
&.optional {background-image: url('../images/gab-icons/icon-search-sprite.svg');}
|
||||
&.groups {background-image: url('../images/gab-icons/icon-explore-sprite.svg');}
|
||||
|
||||
|
||||
&.optional {
|
||||
|
|
|
@ -3,13 +3,27 @@
|
|||
class REST::GroupSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :id, :title, :description, :cover_image_url, :is_archived
|
||||
attributes :id, :title, :description, :cover_image_url, :is_archived, :member_count
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def clean_migrated_url
|
||||
object
|
||||
.cover_image_file_name
|
||||
.sub("gab://groups/", "https://gab.com/media/user/")
|
||||
end
|
||||
|
||||
def cover_image_url
|
||||
if object.cover_image_file_name and object.cover_image_file_name.start_with? "gab://groups/"
|
||||
return clean_migrated_url
|
||||
end
|
||||
|
||||
full_asset_url(object.cover_image.url)
|
||||
end
|
||||
|
||||
def member_count
|
||||
object.accounts.count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -61,7 +61,7 @@ class FanOutOnWriteService < BaseService
|
|||
|
||||
Rails.logger.debug "Delivering status #{status.id} to group"
|
||||
|
||||
# Redis.current.publish("timeline:group:#{status.group_id}", @payload)
|
||||
Redis.current.publish("timeline:group:#{status.group_id}", @payload)
|
||||
end
|
||||
|
||||
def deliver_to_mentioned_followers(status)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
class GroupQueryService < BaseService
|
||||
def call(group)
|
||||
Status.distinct
|
||||
.as_group_timeline(group)
|
||||
Status.as_group_timeline(group)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue