Gab Social. All are welcome.

This commit is contained in:
robcolbert
2019-07-02 03:10:25 -04:00
commit bd0b5afc92
5366 changed files with 222812 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
label: { id: 'groups.new.title_placeholder', defaultMessage: 'New group title' },
title: { id: 'groups.new.create', defaultMessage: 'Add group' },
});
const mapStateToProps = state => ({
value: state.getIn(['groupEditor', 'title']),
disabled: state.getIn(['groupEditor', 'isSubmitting']),
});
const mapDispatchToProps = dispatch => ({
onChange: value => dispatch(changeListEditorTitle(value)),
onSubmit: () => dispatch(submitListEditor(true)),
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class Create extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
disabled: PropTypes.bool,
intl: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
};
handleChange = e => {
this.props.onChange(e.target.value);
}
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
}
handleClick = () => {
this.props.onSubmit();
}
render () {
const { value, disabled, intl } = this.props;
const label = intl.formatMessage(messages.label);
const title = intl.formatMessage(messages.title);
return (
<form className='column-inline-form' onSubmit={this.handleSubmit}>
<label>
<span style={{ display: 'none' }}>{label}</span>
<input
className='setting-text'
value={value}
disabled={disabled}
onChange={this.handleChange}
placeholder={label}
/>
</label>
<IconButton
disabled={disabled}
icon='plus'
title={title}
onClick={this.handleClick}
/>
</form>
);
}
}

View File

@@ -0,0 +1,81 @@
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 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';
const messages = defineMessages({
heading: { id: 'column.groups', defaultMessage: 'Groups' },
subheading: { id: 'groups.subheading', defaultMessage: 'Your groups' },
});
const getOrderedGroups = createSelector([state => state.get('groups')], groups => {
if (!groups) {
return groups;
}
return groups.toList().filter(item => !!item);
});
const mapStateToProps = state => ({
groups: getOrderedGroups(state),
});
export default @connect(mapStateToProps)
@injectIntl
class Groups extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
groups: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
componentWillMount () {
this.props.dispatch(fetchGroups());
}
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>
);
}
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import InnerHeader from './inner_header';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
export default class Header extends ImmutablePureComponent {
static propTypes = {
group: ImmutablePropTypes.map,
relationships: ImmutablePropTypes.map,
toggleMembership: PropTypes.func.isRequired,
};
static contextTypes = {
router: PropTypes.object,
};
render () {
const { group, relationships, toggleMembership } = this.props;
if (group === null) {
return null;
}
return (
<div className='account-timeline__header'>
<InnerHeader
group={group}
relationships={relationships}
toggleMembership={toggleMembership}
/>
<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>
);
}
}

View File

@@ -0,0 +1,96 @@
'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>
);
}
}

View File

@@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import Header from '../components/header';
import { joinGroup, leaveGroup } from '../../../../actions/groups';
const mapStateToProps = (state, { groupId }) => ({
group: state.getIn(['groups', groupId]),
relationships: state.getIn(['group_relationships', groupId]),
});
const mapDispatchToProps = (dispatch, { intl }) => ({
toggleMembership (group, relationships) {
if (relationships.get('member')) {
dispatch(leaveGroup(group.get('id')));
} else {
dispatch(joinGroup(group.get('id')));
}
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Header);

View File

@@ -0,0 +1,105 @@
import React from 'react';
import { connect } from 'react-redux';
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';
const mapStateToProps = (state, props) => ({
group: state.getIn(['groups', 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 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;
dispatch(fetchGroup(id));
dispatch(expandGroupTimeline(id));
this.disconnect = dispatch(connectGroupStream(id));
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
handleLoadMore = maxId => {
const { id } = this.props.params;
this.props.dispatch(expandGroupTimeline(id, { maxId }));
}
render () {
const { hasUnread, columnId, group } = this.props;
const { id } = this.props.params;
const title = group ? group.get('title') : id;
if (typeof group === 'undefined') {
return (
<Column>
<div>
<LoadingIndicator />
</div>
</Column>
);
} else if (group === false) {
return (
<Column>
<ColumnBackButton />
<MissingIndicator />
</Column>
);
}
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>
);
}
}