diff --git a/app/controllers/api/v1/groups/accounts_controller.rb b/app/controllers/api/v1/groups/accounts_controller.rb
index 8a31f9f5..e09b21e2 100644
--- a/app/controllers/api/v1/groups/accounts_controller.rb
+++ b/app/controllers/api/v1/groups/accounts_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/groups_controller.rb b/app/controllers/api/v1/groups_controller.rb
index 8299f788..13f7748e 100644
--- a/app/controllers/api/v1/groups_controller.rb
+++ b/app/controllers/api/v1/groups_controller.rb
@@ -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
diff --git a/app/controllers/api/v1/timelines/group_controller.rb b/app/controllers/api/v1/timelines/group_controller.rb
index e7b36fa8..cbcfb44d 100644
--- a/app/controllers/api/v1/timelines/group_controller.rb
+++ b/app/controllers/api/v1/timelines/group_controller.rb
@@ -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
diff --git a/app/javascript/gabsocial/actions/compose.js b/app/javascript/gabsocial/actions/compose.js
index 34fd54f6..fa995182 100644
--- a/app/javascript/gabsocial/actions/compose.js
+++ b/app/javascript/gabsocial/actions/compose.js
@@ -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']),
diff --git a/app/javascript/gabsocial/actions/groups.js b/app/javascript/gabsocial/actions/groups.js
index 0de668b8..b67a3116 100644
--- a/app/javascript/gabsocial/actions/groups.js
+++ b/app/javascript/gabsocial/actions/groups.js
@@ -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,
+ };
+};
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/compose/components/compose_form.js b/app/javascript/gabsocial/features/compose/components/compose_form.js
index 6d8625e5..5d6a677f 100644
--- a/app/javascript/gabsocial/features/compose/components/compose_form.js
+++ b/app/javascript/gabsocial/features/compose/components/compose_form.js
@@ -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 = () => {
diff --git a/app/javascript/gabsocial/features/compose/containers/compose_form_container.js b/app/javascript/gabsocial/features/compose/containers/compose_form_container.js
index 8395d589..15157b9e 100644
--- a/app/javascript/gabsocial/features/compose/containers/compose_form_container.js
+++ b/app/javascript/gabsocial/features/compose/containers/compose_form_container.js
@@ -33,8 +33,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(changeCompose(text));
},
- onSubmit (router) {
- dispatch(submitCompose(router));
+ onSubmit (router, group) {
+ dispatch(submitCompose(router, group));
},
onClearSuggestions () {
diff --git a/app/javascript/gabsocial/features/groups/index/card.js b/app/javascript/gabsocial/features/groups/index/card.js
new file mode 100644
index 00000000..82a52a16
--- /dev/null
+++ b/app/javascript/gabsocial/features/groups/index/card.js
@@ -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 (
+
+
{coverImageUrl &&
}
+
+
{group.get('title')}
+
{shortNumberFormat(group.get('member_count'))} {intl.formatMessage(messages.members)}{role && ยท {role}}
+
{group.get('description')}
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/groups/index/index.js b/app/javascript/gabsocial/features/groups/index/index.js
index 743a72e2..a582d3ae 100644
--- a/app/javascript/gabsocial/features/groups/index/index.js
+++ b/app/javascript/gabsocial/features/groups/index/index.js
@@ -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 (
-
-
-
- );
- }
-
- const emptyMessage = ;
-
- return (
-
-
-
-
-
-
-
- {groups.map(group =>
-
- )}
-
-
- );
- }
-
-}
+ render () {
+ const { intl, groupIds, activeTab } = this.props;
+
+ return (
+
+
+
{intl.formatMessage(messages.heading)}
+
+
+
+
+ {intl.formatMessage(messages.tab_featured)}
+
+
+
+ {intl.formatMessage(messages.tab_member)}
+
+
+
+ {intl.formatMessage(messages.tab_admin)}
+
+
+
+
+
+
+ {groupIds.map(id => )}
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/gabsocial/features/groups/members/index.js b/app/javascript/gabsocial/features/groups/members/index.js
new file mode 100644
index 00000000..ed396d4f
--- /dev/null
+++ b/app/javascript/gabsocial/features/groups/members/index.js
@@ -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 (
+
+
+
+ );
+ }
+
+ return (
+
+ }
+ >
+ {accountIds.map(id => )}
+
+
+ );
+ }
+}
diff --git a/app/javascript/gabsocial/features/groups/timeline/components/header.js b/app/javascript/gabsocial/features/groups/timeline/components/header.js
index d053bc22..9944cb87 100644
--- a/app/javascript/gabsocial/features/groups/timeline/components/header.js
+++ b/app/javascript/gabsocial/features/groups/timeline/components/header.js
@@ -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 ;
+ } else if (relationships.get('member')) {
+ return ;
+ }
+ }
- return (
-
-
+ render () {
+ const { group, relationships } = this.props;
-
-
-
-
-
- );
- }
+ if (!group || !relationships) {
+ return null;
+ }
+ return (
+
+
+
+
+
+
+
+ Posts
+ Members
+ {this.getActionButton()}
+
+
+
+ );
+ }
}
diff --git a/app/javascript/gabsocial/features/groups/timeline/components/inner_header.js b/app/javascript/gabsocial/features/groups/timeline/components/inner_header.js
deleted file mode 100644
index f87b0da8..00000000
--- a/app/javascript/gabsocial/features/groups/timeline/components/inner_header.js
+++ /dev/null
@@ -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();
- }
-
- if (!relationships) { // Wait until the relationship is loaded
- actionBtn = '';
- } else if (!relationships.get('member')) {
- actionBtn =