Commit 79961d1b authored by Rob Colbert's avatar Rob Colbert

Merge branch 'feature/groups-improvements' into 'develop'

minor groups + quoting improvements

See merge request gab/social/gab-social!40
parents c8ce87e4 a12fa057
...@@ -26,7 +26,8 @@ class Api::V1::Groups::AccountsController < Api::BaseController ...@@ -26,7 +26,8 @@ class Api::V1::Groups::AccountsController < Api::BaseController
def update def update
authorize @group, :update_account? 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 render_empty
end end
......
...@@ -51,6 +51,10 @@ export const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST'; ...@@ -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_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS';
export const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; 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) => { export const fetchGroup = id => (dispatch, getState) => {
if (!me) return; if (!me) return;
...@@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) { ...@@ -521,4 +525,43 @@ export function groupRemoveStatusFail(groupId, id, error) {
id, id,
error, 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
...@@ -19,7 +19,6 @@ const messages = defineMessages({ ...@@ -19,7 +19,6 @@ const messages = defineMessages({
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
share: { id: 'status.share', defaultMessage: 'Share' },
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
...@@ -93,15 +92,6 @@ class StatusActionBar extends ImmutablePureComponent { ...@@ -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 = () => { handleFavouriteClick = () => {
if (me) { if (me) {
this.props.onFavourite(this.props.status); this.props.onFavourite(this.props.status);
...@@ -280,10 +270,6 @@ class StatusActionBar extends ImmutablePureComponent { ...@@ -280,10 +270,6 @@ class StatusActionBar extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && (
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
);
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<div className='status__action-bar__counter'> <div className='status__action-bar__counter'>
...@@ -301,7 +287,6 @@ class StatusActionBar extends ImmutablePureComponent { ...@@ -301,7 +287,6 @@ class StatusActionBar extends ImmutablePureComponent {
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> <IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
{favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>} {favoriteCount !== 0 && <span className='detailed-status__link'>{favoriteCount}</span>}
</div> </div>
{shareButton}
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>
<DropdownMenuContainer status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} /> <DropdownMenuContainer status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
......
...@@ -8,14 +8,18 @@ import LoadingIndicator from '../../../components/loading_indicator'; ...@@ -8,14 +8,18 @@ import LoadingIndicator from '../../../components/loading_indicator';
import { import {
fetchMembers, fetchMembers,
expandMembers, expandMembers,
updateRole,
createRemovedAccount,
} from '../../../actions/groups'; } from '../../../actions/groups';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container'; import AccountContainer from '../../../containers/account_container';
import Column from '../../ui/components/column'; import Column from '../../ui/components/column';
import ScrollableList from '../../../components/scrollable_list'; import ScrollableList from '../../../components/scrollable_list';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
const mapStateToProps = (state, { params: { id } }) => ({ const mapStateToProps = (state, { params: { id } }) => ({
group: state.getIn(['groups', id]), group: state.getIn(['groups', id]),
relationships: state.getIn(['group_relationships', id]),
accountIds: state.getIn(['user_lists', 'groups', id, 'items']), accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']), hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
}); });
...@@ -47,9 +51,9 @@ class GroupMembers extends ImmutablePureComponent { ...@@ -47,9 +51,9 @@ class GroupMembers extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { accountIds, hasMore, group } = this.props; const { accountIds, hasMore, group, relationships, dispatch } = this.props;
if (!group || !accountIds) { if (!group || !accountIds || !relationships) {
return ( return (
<Column> <Column>
<LoadingIndicator /> <LoadingIndicator />
...@@ -65,7 +69,23 @@ class GroupMembers extends ImmutablePureComponent { ...@@ -65,7 +69,23 @@ class GroupMembers extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />} emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />}
> >
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} {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 (
<div className="group-account-wrapper" key={id}>
<AccountContainer id={id} withNote={false} actionIcon="none" onActionClick={() => true} />
{menu.length > 0 && <DropdownMenuContainer items={menu} icon='ellipsis-h' size={18} direction='right' />}
</div>
);
})}
</ScrollableList> </ScrollableList>
</Column> </Column>
); );
......
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 (
<div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
</div>
</div>
);
}
}
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);
...@@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; ...@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusListContainer from '../../ui/containers/status_list_container'; import StatusListContainer from '../../ui/containers/status_list_container';
import Column from '../../../components/column'; import Column from '../../../components/column';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { connectGroupStream } from '../../../actions/streaming'; import { connectGroupStream } from '../../../actions/streaming';
import { expandGroupTimeline } from '../../../actions/timelines'; import { expandGroupTimeline } from '../../../actions/timelines';
import MissingIndicator from '../../../components/missing_indicator'; import MissingIndicator from '../../../components/missing_indicator';
...@@ -12,6 +12,16 @@ import LoadingIndicator from '../../../components/loading_indicator'; ...@@ -12,6 +12,16 @@ import LoadingIndicator from '../../../components/loading_indicator';
import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container'; import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container';
import { me } from 'gabsocial/initial_state'; import { me } from 'gabsocial/initial_state';
import Avatar from '../../../components/avatar'; 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) => ({ const mapStateToProps = (state, props) => ({
account: state.getIn(['accounts', me]), account: state.getIn(['accounts', me]),
...@@ -38,6 +48,10 @@ class GroupTimeline extends React.PureComponent { ...@@ -38,6 +48,10 @@ class GroupTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
state = {
collapsed: true,
}
componentDidMount () { componentDidMount () {
const { dispatch } = this.props; const { dispatch } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
...@@ -59,8 +73,14 @@ class GroupTimeline extends React.PureComponent { ...@@ -59,8 +73,14 @@ class GroupTimeline extends React.PureComponent {
this.props.dispatch(expandGroupTimeline(id, { maxId })); this.props.dispatch(expandGroupTimeline(id, { maxId }));
} }
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed });
}
render () { 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; const { id } = this.props.params;
if (typeof group === 'undefined' || !relationships) { if (typeof group === 'undefined' || !relationships) {
...@@ -89,6 +109,31 @@ class GroupTimeline extends React.PureComponent { ...@@ -89,6 +109,31 @@ class GroupTimeline extends React.PureComponent {
)} )}
<div className='group__feed'> <div className='group__feed'>
<div className="column-header__wrapper">
<h1 className="column-header">
<Link to={`/groups/${id}`} className={classNames('btn grouped active')}>
{intl.formatMessage(messages.tabLatest)}
</Link>
<div className='column-header__buttons'>
<button
className={classNames('column-header__button', { 'active': !collapsed })}
title={intl.formatMessage(collapsed ? messages.show : messages.hide)}
aria-label={intl.formatMessage(collapsed ? messages.show : messages.hide)}
aria-pressed={collapsed ? 'false' : 'true'}
onClick={this.handleToggleClick}
><Icon id='sliders' /></button>
</div>
</h1>
{!collapsed && <div className='column-header__collapsible'>
<div className='column-header__collapsible-inner'>
<div className='column-header__collapsible__extra'>
<ColumnSettingsContainer />
</div>
</div>
</div>}
</div>
<StatusListContainer <StatusListContainer
alwaysPrepend alwaysPrepend
scrollKey={`group_timeline-${columnId}`} scrollKey={`group_timeline-${columnId}`}
......
...@@ -8,8 +8,8 @@ import { dequeueTimeline } from 'gabsocial/actions/timelines'; ...@@ -8,8 +8,8 @@ import { dequeueTimeline } from 'gabsocial/actions/timelines';
import { scrollTopTimeline } from '../../../actions/timelines'; import { scrollTopTimeline } from '../../../actions/timelines';
const makeGetStatusIds = () => createSelector([ const makeGetStatusIds = () => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()), (state, { type, id }) => state.getIn(['settings', type], ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), (state, { type, id }) => state.getIn(['timelines', id, 'items'], ImmutableList()),
(state) => state.get('statuses'), (state) => state.get('statuses'),
], (columnSettings, statusIds, statuses) => { ], (columnSettings, statusIds, statuses) => {
return statusIds.filter(id => { return statusIds.filter(id => {
...@@ -34,7 +34,7 @@ const mapStateToProps = (state, {timelineId}) => { ...@@ -34,7 +34,7 @@ const mapStateToProps = (state, {timelineId}) => {
const getStatusIds = makeGetStatusIds(); const getStatusIds = makeGetStatusIds();
return { 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), isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true),
isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false),
hasMore: state.getIn(['timelines', timelineId, 'hasMore']), hasMore: state.getIn(['timelines', timelineId, 'hasMore']),
......
...@@ -24,6 +24,12 @@ const initialState = ImmutableMap({ ...@@ -24,6 +24,12 @@ const initialState = ImmutableMap({
}), }),
}), }),
group: ImmutableMap({
shows: ImmutableMap({
reply: true,
}),
}),
notifications: ImmutableMap({ notifications: ImmutableMap({
alerts: ImmutableMap({ alerts: ImmutableMap({
follow: true, follow: true,
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
@import 'gabsocial/components/compose-form'; @import 'gabsocial/components/compose-form';
@import 'gabsocial/components/group-card'; @import 'gabsocial/components/group-card';
@import 'gabsocial/components/group-detail'; @import 'gabsocial/components/group-detail';
@import 'gabsocial/components/group-accounts';
@import 'gabsocial/components/group-form'; @import 'gabsocial/components/group-form';
@import 'gabsocial/components/group-sidebar-panel'; @import 'gabsocial/components/group-sidebar-panel';
......
.group-account-wrapper {
position: relative;
& > div > .icon-button {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
}
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment