diff --git a/app/controllers/api/v1/groups/accounts_controller.rb b/app/controllers/api/v1/groups/accounts_controller.rb index e09b21e2..61759000 100644 --- a/app/controllers/api/v1/groups/accounts_controller.rb +++ b/app/controllers/api/v1/groups/accounts_controller.rb @@ -26,7 +26,8 @@ class Api::V1::Groups::AccountsController < Api::BaseController def update authorize @group, :update_account? - GroupAccount.where(group: @group, account_id: current_account.id).update(group_account_params) + @account = @group.accounts.find(params[:account_id]) + GroupAccount.where(group: @group, account: @account).update(group_account_params) render_empty end diff --git a/app/javascript/gabsocial/actions/groups.js b/app/javascript/gabsocial/actions/groups.js index eac34371..01ac5503 100644 --- a/app/javascript/gabsocial/actions/groups.js +++ b/app/javascript/gabsocial/actions/groups.js @@ -51,6 +51,10 @@ export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST'; export const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS'; export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; +export const GROUP_UPDATE_ROLE_REQUEST = 'GROUP_UPDATE_ROLE_REQUEST'; +export const GROUP_UPDATE_ROLE_SUCCESS = 'GROUP_UPDATE_ROLE_SUCCESS'; +export const GROUP_UPDATE_ROLE_FAIL = 'GROUP_UPDATE_ROLE_FAIL'; + export const fetchGroup = id => (dispatch, getState) => { if (!me) return; @@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) { id, error, }; +}; + +export function updateRole(groupId, id, role) { + return (dispatch, getState) => { + if (!me) return; + + dispatch(updateRoleRequest(groupId, id)); + + api(getState).patch(`/api/v1/groups/${groupId}/accounts?account_id=${id}`, { role }).then(response => { + dispatch(updateRoleSuccess(groupId, id)); + }).catch(error => { + dispatch(updateRoleFail(groupId, id, error)); + }); + }; +}; + +export function updateRoleRequest(groupId, id) { + return { + type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST, + groupId, + id, + }; +}; + +export function updateRoleSuccess(groupId, id) { + return { + type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS, + groupId, + id, + }; +}; + +export function updateRoleFail(groupId, id, error) { + return { + type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL, + groupId, + id, + error, + }; }; \ No newline at end of file diff --git a/app/javascript/gabsocial/components/status_action_bar.js b/app/javascript/gabsocial/components/status_action_bar.js index e3384c72..42dfa150 100644 --- a/app/javascript/gabsocial/components/status_action_bar.js +++ b/app/javascript/gabsocial/components/status_action_bar.js @@ -19,7 +19,6 @@ const messages = defineMessages({ mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, - share: { id: 'status.share', defaultMessage: 'Share' }, more: { id: 'status.more', defaultMessage: 'More' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, @@ -93,15 +92,6 @@ class StatusActionBar extends ImmutablePureComponent { } } - handleShareClick = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }).catch((e) => { - if (e.name !== 'AbortError') console.error(e); - }); - } - handleFavouriteClick = () => { if (me) { this.props.onFavourite(this.props.status); @@ -280,10 +270,6 @@ class StatusActionBar extends ImmutablePureComponent { replyTitle = intl.formatMessage(messages.replyAll); } - const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( - - ); - return (
@@ -301,7 +287,6 @@ class StatusActionBar extends ImmutablePureComponent { {favoriteCount !== 0 && {favoriteCount}}
- {shareButton}
diff --git a/app/javascript/gabsocial/features/groups/members/index.js b/app/javascript/gabsocial/features/groups/members/index.js index ed396d4f..31cdb738 100644 --- a/app/javascript/gabsocial/features/groups/members/index.js +++ b/app/javascript/gabsocial/features/groups/members/index.js @@ -8,14 +8,18 @@ import LoadingIndicator from '../../../components/loading_indicator'; import { fetchMembers, expandMembers, + updateRole, + createRemovedAccount, } 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'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; const mapStateToProps = (state, { params: { id } }) => ({ group: state.getIn(['groups', id]), + relationships: state.getIn(['group_relationships', id]), accountIds: state.getIn(['user_lists', 'groups', id, 'items']), hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']), }); @@ -47,9 +51,9 @@ class GroupMembers extends ImmutablePureComponent { }, 300, { leading: true }); render () { - const { accountIds, hasMore, group } = this.props; - - if (!group || !accountIds) { + const { accountIds, hasMore, group, relationships, dispatch } = this.props; + + if (!group || !accountIds || !relationships) { return ( @@ -65,7 +69,23 @@ class GroupMembers extends ImmutablePureComponent { onLoadMore={this.handleLoadMore} emptyMessage={} > - {accountIds.map(id => )} + {accountIds.map(id => { + let menu = []; + + if (relationships.get('admin')) { + menu = [ + { text: 'Remove from group', action: () => dispatch(createRemovedAccount(group.get('id'), id)) }, + { text: 'Make administrator', action: () => dispatch(updateRole(group.get('id'), id, 'admin')) }, + ] + } + + return ( +
+ true} /> + {menu.length > 0 && } +
+ ); + })}
); diff --git a/app/javascript/gabsocial/features/groups/timeline/components/column_settings.js b/app/javascript/gabsocial/features/groups/timeline/components/column_settings.js new file mode 100644 index 00000000..d0ae58bc --- /dev/null +++ b/app/javascript/gabsocial/features/groups/timeline/components/column_settings.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import SettingToggle from '../../../notifications/components/setting_toggle'; + +export default @injectIntl +class ColumnSettings extends React.PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + render () { + const { settings, onChange } = this.props; + + return ( +
+ + +
+ } /> +
+
+ ); + } + +} diff --git a/app/javascript/gabsocial/features/groups/timeline/containers/column_settings_container.js b/app/javascript/gabsocial/features/groups/timeline/containers/column_settings_container.js new file mode 100644 index 00000000..d320b40e --- /dev/null +++ b/app/javascript/gabsocial/features/groups/timeline/containers/column_settings_container.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import ColumnSettings from '../components/column_settings'; +import { changeSetting, saveSettings } from '../../../../actions/settings'; + +const mapStateToProps = state => ({ + settings: state.getIn(['settings', 'group']), +}); + +const mapDispatchToProps = dispatch => ({ + + onChange (key, checked) { + dispatch(changeSetting(['group', ...key], checked)); + }, + + onSave () { + dispatch(saveSettings()); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/javascript/gabsocial/features/groups/timeline/index.js b/app/javascript/gabsocial/features/groups/timeline/index.js index 0dc648fe..fde6c2dc 100644 --- a/app/javascript/gabsocial/features/groups/timeline/index.js +++ b/app/javascript/gabsocial/features/groups/timeline/index.js @@ -4,7 +4,7 @@ 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 { FormattedMessage, injectIntl } from 'react-intl'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { connectGroupStream } from '../../../actions/streaming'; import { expandGroupTimeline } from '../../../actions/timelines'; import MissingIndicator from '../../../components/missing_indicator'; @@ -12,6 +12,16 @@ import LoadingIndicator from '../../../components/loading_indicator'; import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container'; import { me } from 'gabsocial/initial_state'; import Avatar from '../../../components/avatar'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; +import ColumnSettingsContainer from "./containers/column_settings_container"; +import Icon from 'gabsocial/components/icon'; + +const messages = defineMessages({ + tabLatest: { id: 'group.timeline.tab_latest', defaultMessage: 'Latest' }, + show: { id: 'group.timeline.show_settings', defaultMessage: 'Show settings' }, + hide: { id: 'group.timeline.hide_settings', defaultMessage: 'Hide settings' }, +}); const mapStateToProps = (state, props) => ({ account: state.getIn(['accounts', me]), @@ -38,6 +48,10 @@ class GroupTimeline extends React.PureComponent { intl: PropTypes.object.isRequired, }; + state = { + collapsed: true, + } + componentDidMount () { const { dispatch } = this.props; const { id } = this.props.params; @@ -59,8 +73,14 @@ class GroupTimeline extends React.PureComponent { this.props.dispatch(expandGroupTimeline(id, { maxId })); } + handleToggleClick = (e) => { + e.stopPropagation(); + this.setState({ collapsed: !this.state.collapsed }); + } + render () { - const { columnId, group, relationships, account } = this.props; + const { columnId, group, relationships, account, intl } = this.props; + const { collapsed } = this.state; const { id } = this.props.params; if (typeof group === 'undefined' || !relationships) { @@ -89,6 +109,31 @@ class GroupTimeline extends React.PureComponent { )}
+
+

+ + {intl.formatMessage(messages.tabLatest)} + + +
+ +
+

+ {!collapsed &&
+
+
+ +
+
+
} +
+ createSelector([ - (state, { type }) => state.getIn(['settings', type], ImmutableMap()), - (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), + (state, { type, id }) => state.getIn(['settings', type], ImmutableMap()), + (state, { type, id }) => state.getIn(['timelines', id, 'items'], ImmutableList()), (state) => state.get('statuses'), ], (columnSettings, statusIds, statuses) => { return statusIds.filter(id => { @@ -34,7 +34,7 @@ const mapStateToProps = (state, {timelineId}) => { const getStatusIds = makeGetStatusIds(); return { - statusIds: getStatusIds(state, { type: timelineId }), + statusIds: getStatusIds(state, { type: timelineId.substring(0,5) === 'group' ? 'group' : timelineId, id: timelineId }), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), diff --git a/app/javascript/gabsocial/reducers/settings.js b/app/javascript/gabsocial/reducers/settings.js index 49ef639f..575943ba 100644 --- a/app/javascript/gabsocial/reducers/settings.js +++ b/app/javascript/gabsocial/reducers/settings.js @@ -24,6 +24,12 @@ const initialState = ImmutableMap({ }), }), + group: ImmutableMap({ + shows: ImmutableMap({ + reply: true, + }), + }), + notifications: ImmutableMap({ alerts: ImmutableMap({ follow: true, diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 2ae7da3e..ab469abd 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -28,6 +28,7 @@ @import 'gabsocial/components/compose-form'; @import 'gabsocial/components/group-card'; @import 'gabsocial/components/group-detail'; +@import 'gabsocial/components/group-accounts'; @import 'gabsocial/components/group-form'; @import 'gabsocial/components/group-sidebar-panel'; diff --git a/app/javascript/styles/gabsocial/components/group-accounts.scss b/app/javascript/styles/gabsocial/components/group-accounts.scss new file mode 100644 index 00000000..c858b2e6 --- /dev/null +++ b/app/javascript/styles/gabsocial/components/group-accounts.scss @@ -0,0 +1,10 @@ +.group-account-wrapper { + position: relative; + + & > div > .icon-button { + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%); + } +} \ No newline at end of file