Merge branch 'develop' of https://code.gab.com/gab/social/gab-social into feature/frontend_refactor

This commit is contained in:
mgabdev
2020-01-28 11:29:26 -05:00
225 changed files with 5598 additions and 2652 deletions

View File

@@ -5,9 +5,11 @@ import { autoPlayGif, displayMedia } from 'gabsocial/initial_state';
import classNames from 'classnames';
import { decode } from 'blurhash';
import { isIOS } from 'gabsocial/utils/is_mobile';
import conversations_list_container from '../../direct_timeline/containers/conversations_list_container';
import './media_item.scss';
export default class MediaItem extends ImmutablePureComponent {
static propTypes = {
@@ -125,8 +127,10 @@ export default class MediaItem extends ImmutablePureComponent {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
autoPlay={autoPlay}
preload='auto'
loop
muted
playsInline
/>
<span className='media-gallery__gifv__label'>GIF</span>

View File

@@ -44,7 +44,7 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), to: '/filters' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.keyboard_shortcuts), action: this.handleHotkeyClick });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });

View File

@@ -18,9 +18,13 @@ import { isMobile } from '../../../../utils/is_mobile';
import { countableText } from '../../util/counter';
import Icon from '../../../../components/icon';
import Button from '../../../../components/button';
import SchedulePostDropdownContainer from '../containers/schedule_post_dropdown_container';
import UploadFormContainer from '../containers/upload_form_container';
import QuotedStatusPreviewContainer from '../containers/quoted_status_preview_container';
import './compose_form.scss';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const maxPostCharacterCount = 3000;
@@ -29,6 +33,7 @@ const messages = defineMessages({
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
publish: { id: 'compose_form.publish', defaultMessage: 'Gab' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' }
});
export default @injectIntl
@@ -44,6 +49,7 @@ class ComposeForm extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
edit: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired,
suggestions: ImmutablePropTypes.list,
spoiler: PropTypes.bool,
@@ -69,6 +75,8 @@ class ComposeForm extends ImmutablePureComponent {
autoFocus: PropTypes.bool,
group: ImmutablePropTypes.map,
isModalOpen: PropTypes.bool,
scheduledAt: PropTypes.instanceOf(Date),
setScheduledAt: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -93,12 +101,21 @@ class ComposeForm extends ImmutablePureComponent {
handleClick = (e) => {
if (!this.form) return false;
if (e.target) {
if (e.target.classList.contains('react-datepicker__time-list-item')) return;
}
if (!this.form.contains(e.target)) {
this.handleClickOutside();
}
}
handleClickOutside = () => {
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props;
const condensed = shouldCondense && !text;
if (condensed && scheduledAt && !isModalOpen) { //Reset scheduled date if condensing
this.props.setScheduledAt(null);
}
this.setState({
composeFocused: false,
});
@@ -198,7 +215,7 @@ class ComposeForm extends ImmutablePureComponent {
}
render () {
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen } = this.props;
const { intl, onPaste, showSearch, anyMedia, shouldCondense, autoFocus, isModalOpen, quoteOfId, edit, scheduledAt } = this.props;
const condensed = shouldCondense && !this.props.text && !this.state.composeFocused;
const disabled = this.props.isSubmitting;
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
@@ -213,6 +230,10 @@ class ComposeForm extends ImmutablePureComponent {
publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
}
if (scheduledAt) {
publishText = intl.formatMessage(messages.schedulePost);
}
const composeClassNames = classNames({
'compose-form': true,
'condensed': condensed,
@@ -265,20 +286,25 @@ class ComposeForm extends ImmutablePureComponent {
{
!condensed &&
<div className='compose-form__modifiers'>
<UploadForm />
<PollFormContainer />
<UploadFormContainer />
{!edit && <PollFormContainer />}
</div>
}
</AutosuggestTextbox>
{quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} />}
{
!condensed &&
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'>
<UploadButtonContainer />
<PollButtonContainer />
{!edit && <PollButtonContainer />}
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
<SchedulePostDropdownContainer
position={isModalOpen ? 'top' : undefined}
/>
</div>
<CharacterCounter max={maxPostCharacterCount} text={text} />
</div>

View File

@@ -0,0 +1,26 @@
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content';
export default class QuotedStatusPreview extends React.PureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
}
render() {
const { status, account } = this.props;
return (
<div className='compose-form__quote-preview'>
<DisplayName account={account} />
<StatusContent
status={status}
expanded={false}
/>
</div>
);
}
}

View File

@@ -0,0 +1,114 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import DatePicker from 'react-datepicker';
import IconButton from '../../../components/icon_button';
import { isMobile } from '../../../is_mobile';
import "react-datepicker/dist/react-datepicker.css";
const messages = defineMessages({
schedule_status: { id: 'schedule_status.title', defaultMessage: 'Schedule Status' },
});
class DatePickerWrapper extends React.PureComponent {
static propTypes = {
value: PropTypes.string,
onClick: PropTypes.func,
};
render() {
const { value, onClick } = this.props;
return (
<button className="schedule-post-dropdown-wrapper" onClick={onClick}>
{value}
</button>
)
}
}
export default @injectIntl
class SchedulePostDropdown extends React.PureComponent {
static propTypes = {
date: PropTypes.instanceOf(Date),
setScheduledAt: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isPro: PropTypes.bool,
onOpenProUpgradeModal: PropTypes.func.isRequired,
position: PropTypes.string,
};
handleToggle = () => {
if (!this.props.isPro) {
return this.props.onOpenProUpgradeModal();
}
const { date } = this.props;
const value = date ? null : new Date();
this.handleSetDate(value);
}
handleSetDate = (date) => {
this.props.setScheduledAt(date);
}
render () {
const { intl, date, isPro, position } = this.props;
const open = !!date;
const datePickerDisabled = !isPro;
const withPortal = isMobile(window.innerWidth);
const popperPlacement = position || undefined;
return (
<div className='schedule-post-dropdown'>
<div className='schedule-post-dropdown__container'>
<IconButton
inverted
className='schedule-post-dropdown__icon'
icon='calendar'
title={intl.formatMessage(messages.schedule_status)}
size={18}
expanded={open}
active={open}
onClick={this.handleToggle}
style={{ height: null, lineHeight: '27px' }}
/>
</div>
{
open &&
<DatePicker
target={this}
className='schedule-post-dropdown__datepicker'
minDate={new Date()}
selected={date}
onChange={date => this.handleSetDate(date)}
timeFormat="p"
timeIntervals={15}
timeCaption="Time"
dateFormat="MMM d, yyyy h:mm aa"
disabled={datePickerDisabled}
showTimeSelect
customInput={<DatePickerWrapper />}
withPortal={withPortal}
popperPlacement={popperPlacement}
popperModifiers={{
offset: {
enabled: true,
offset: "0px, 5px"
},
preventOverflow: {
enabled: true,
escapeWithReference: false,
boundariesElement: "viewport"
}
}}
/>
}
</div>
);
}
}

View File

@@ -1,18 +1,19 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../../containers/account_container';
import StatusContainer from '../../../../containers/status_container';
import TrendingItem from '../../../../components/trending_item';
import Icon from '../../../../components/icon';
import { WhoToFollowPanel } from '../../../../components/panel';
import Hashtag from '../../../components/hashtag';
import Icon from 'gabsocial/components/icon';
import WhoToFollowPanel from '../../ui/components/who_to_follow_panel';
// import TrendsPanel from '../../ui/components/trends_panel';
import GroupListItem from 'gabsocial/components/group_list_item';
import './search_results.scss';
export default class SearchResults extends ImmutablePureComponent {
export default
@injectIntl
class SearchResults extends ImmutablePureComponent {
static propTypes = {
results: ImmutablePropTypes.map.isRequired,
intl: PropTypes.object.isRequired,
location: PropTypes.object,
};
state = {
@@ -20,7 +21,7 @@ export default class SearchResults extends ImmutablePureComponent {
}
render () {
const { results } = this.props;
const { results, location } = this.props;
const { isSmallScreen } = this.state;
if (results.isEmpty() && isSmallScreen) {
@@ -31,44 +32,46 @@ export default class SearchResults extends ImmutablePureComponent {
);
}
let accounts, statuses, hashtags;
const pathname = location.pathname || '';
const showPeople = pathname === '/search/people';
const showHashtags = pathname === '/search/hashtags';
const showGroups = pathname === '/search/groups';
const isTop = !showPeople && !showHashtags && !showGroups;
let accounts, statuses, hashtags, groups;
let count = 0;
if (results.get('accounts') && results.get('accounts').size > 0) {
count += results.get('accounts').size;
if (results.get('accounts') && results.get('accounts').size > 0 && (isTop || showPeople)) {
const size = isTop ? Math.min(results.get('accounts').size, 5) : results.get('accounts').size;
count += size;
accounts = (
<div className='search-results__section'>
<h5>
<Icon id='users' fixedWidth />
<FormattedMessage id='search_results.accounts' defaultMessage='People' />
</h5>
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
<h5><Icon id='user' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
{results.get('accounts').slice(0, size).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
</div>
);
}
if (results.get('statuses') && results.get('statuses').size > 0) {
count += results.get('statuses').size;
statuses = (
if (results.get('groups') && results.get('groups').size > 0 && (isTop || showGroups)) {
const size = isTop ? Math.min(results.get('groups').size, 5) : results.get('groups').size;
count += size;
groups = (
<div className='search-results__section'>
<h5>
<Icon id='quote-right' fixedWidth />
<FormattedMessage id='search_results.statuses' defaultMessage='Gabs' />
</h5>
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.groups' defaultMessage='Groups' /></h5>
{results.get('groups').slice(0, size).map(group => <GroupListItem key={`search-${group.get('name')}`} group={group} />)}
</div>
);
}
if (results.get('hashtags') && results.get('hashtags').size > 0) {
count += results.get('hashtags').size;
if (results.get('hashtags') && results.get('hashtags').size > 0 && (isTop || showHashtags)) {
const size = isTop ? Math.min(results.get('hashtags').size, 5) : results.get('hashtags').size;
count += size;
hashtags = (
<div className='search-results__section'>
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
{results.get('hashtags').map(hashtag => <TrendingItem key={hashtag.get('name')} hashtag={hashtag} />)}
{results.get('hashtags').slice(0, size).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>
);
}
@@ -87,6 +90,7 @@ export default class SearchResults extends ImmutablePureComponent {
</div>
{accounts}
{groups}
{statuses}
{hashtags}
</div>

View File

@@ -10,8 +10,7 @@ import './upload.scss';
const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
undo: { id: 'upload_form.undo', defaultMessage: 'Delete' },
focus: { id: 'upload_form.focus', defaultMessage: 'Crop' },
delete: { id: 'upload_form.undo', defaultMessage: 'Delete' },
});
export default @injectIntl
@@ -100,24 +99,10 @@ class Upload extends ImmutablePureComponent {
<div className='compose-form-upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form-upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className={classNames('compose-form-upload__actions', { active })}>
<IconButton
onClick={this.handleUndoClick}
icon='times'
title={intl.formatMessage(messages.undo)}
text={intl.formatMessage(messages.undo)}
/>
{
media.get('type') === 'image' &&
<IconButton
onClick={this.handleFocalPointClick}
icon='crosshairs'
title={intl.formatMessage(messages.focus)}
text={intl.formatMessage(messages.focus)}
/>
}
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className={classNames('compose-form__upload__actions', { active })}>
<button className='icon-button' title={intl.formatMessage(messages.delete)} onClick={this.handleUndoClick}><Icon id='times'/></button>
{media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
</div>
<div className={classNames('compose-form-upload__description', { active })}>

View File

@@ -8,9 +8,11 @@ import {
changeComposeSpoilerText,
insertEmojiCompose,
uploadCompose,
changeScheduledAt,
} from '../../../actions/compose';
const mapStateToProps = state => ({
edit: state.getIn(['compose', 'id']) !== null,
text: state.getIn(['compose', 'text']),
suggestions: state.getIn(['compose', 'suggestions']),
spoiler: state.getIn(['compose', 'spoiler']),
@@ -25,6 +27,8 @@ const mapStateToProps = state => ({
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isModalOpen: state.get('modal').modalType === 'COMPOSE',
quoteOfId: state.getIn(['compose', 'quote_of_id']),
scheduledAt: state.getIn(['compose', 'scheduled_at']),
});
const mapDispatchToProps = (dispatch) => ({
@@ -61,6 +65,9 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(insertEmojiCompose(position, data, needsSpace));
},
setScheduledAt (date) {
dispatch(changeScheduledAt(date));
},
});
function mergeProps(stateProps, dispatchProps, ownProps) {

View File

@@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import QuotedStatusPreview from '../components/quoted_status_preview';
const mapStateToProps = (state, { id }) => ({
status: state.getIn(['statuses', id]),
account: state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
});
export default connect(mapStateToProps)(QuotedStatusPreview);

View File

@@ -0,0 +1,22 @@
import { connect } from 'react-redux';
import SchedulePostDropdown from '../components/schedule_post_dropdown';
import { changeScheduledAt } from '../../../actions/compose';
import { openModal } from '../../../actions/modal';
import { me } from '../../../initial_state';
const mapStateToProps = state => ({
date: state.getIn(['compose', 'scheduled_at']),
isPro: state.getIn(['accounts', me, 'is_pro']),
});
const mapDispatchToProps = dispatch => ({
setScheduledAt (date) {
dispatch(changeScheduledAt(date));
},
onOpenProUpgradeModal() {
dispatch(openModal('PRO_UPGRADE'));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(SchedulePostDropdown);

View File

@@ -1,5 +1,6 @@
import SearchResults from '../components/search_results';
import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
import { withRouter } from 'react-router-dom';
const mapStateToProps = state => ({
results: state.getIn(['search', 'results']),
@@ -11,4 +12,4 @@ const mapDispatchToProps = dispatch => ({
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SearchResults));

View File

@@ -6,17 +6,20 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames';
import GroupCard from './card';
import GroupCreate from '../create';
import { me } from 'gabsocial/initial_state';
import { openModal } from '../../../actions/modal';
const messages = defineMessages({
heading: { id: 'column.groups', defaultMessage: 'Groups' },
create: { id: 'groups.create', defaultMessage: 'Create group' },
tab_featured: { id: 'groups.tab_featured', defaultMessage: 'Featured' },
tab_member: { id: 'groups.tab_member', defaultMessage: 'Groups you\'re in' },
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Groups you manage' },
tab_member: { id: 'groups.tab_member', defaultMessage: 'Member' },
tab_admin: { id: 'groups.tab_admin', defaultMessage: 'Manage' },
});
const mapStateToProps = (state, { activeTab }) => ({
groupIds: state.getIn(['group_lists', activeTab]),
account: state.getIn(['accounts', me]),
});
export default @connect(mapStateToProps)
@@ -42,12 +45,27 @@ class Groups extends ImmutablePureComponent {
}
}
handleOpenProUpgradeModal = () => {
this.props.dispatch(openModal('PRO_UPGRADE'));
}
renderHeader() {
const { intl, activeTab } = this.props;
const { intl, activeTab, account, onOpenProUpgradeModal } = this.props;
const isPro = account.get('is_pro');
return (
<div className="group-column-header">
<div className="group-column-header__cta"><Link to="/groups/create" className="button">{intl.formatMessage(messages.create)}</Link></div>
<div className="group-column-header__cta">
{
account && isPro &&
<Link to="/groups/create" className="button standard-small">{intl.formatMessage(messages.create)}</Link>
}
{
account && !isPro &&
<button onClick={this.handleOpenProUpgradeModal} className="button standard-small">{intl.formatMessage(messages.create)}</button>
}
</div>
<div className="group-column-header__title">{intl.formatMessage(messages.heading)}</div>
<div className="column-header__wrapper">

View File

@@ -3,18 +3,22 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import ColumnIndicator from '../../../components/column_indicator';
import {
fetchMembers,
expandMembers,
fetchMembers,
expandMembers,
updateRole,
createRemovedAccount,
} from '../../../actions/groups';
import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import Column from '../../../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]),
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
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']),
});
export default @connect(mapStateToProps)
@@ -44,24 +48,44 @@ class GroupMembers extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { accountIds, hasMore, group } = this.props;
const { accountIds, hasMore, group, relationships, dispatch } = this.props;
if (!group || !accountIds || !relationships) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
if (!group || !accountIds) {
return (<ColumnIndicator type='loading' />);
}
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 => {
let menu = [];
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>
);
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>
</Column>
);
}
}

View File

@@ -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 (
<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>
);
}
}

View File

@@ -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);

View File

@@ -1,11 +1,24 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusListContainer from '../../../containers/status_list_container';
import { FormattedMessage, injectIntl } from 'react-intl';
import StatusListContainer from '../../ui/containers/status_list_container';
import Column from '../../../components/column';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { connectGroupStream } from '../../../actions/streaming';
import { expandGroupTimeline } from '../../../actions/timelines';
import ColumnIndicator from '../../../components/column_indicator';
import TimelineComposeBlock from '../../../components/timeline_compose_block';
import MissingIndicator from '../../../components/missing_indicator';
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) => ({
group: state.getIn(['groups', props.params.id]),
@@ -31,6 +44,10 @@ class GroupTimeline extends ImmutablePureComponent {
intl: PropTypes.object.isRequired,
};
state = {
collapsed: true,
}
componentDidMount () {
const { dispatch } = this.props;
const { id } = this.props.params;
@@ -52,9 +69,15 @@ class GroupTimeline extends ImmutablePureComponent {
this.props.dispatch(expandGroupTimeline(id, { maxId }));
}
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed });
}
render () {
const { columnId, group, relationships } = this.props;
const { id } = this.props.params;
const { columnId, group, relationships, account, intl } = this.props;
const { collapsed } = this.state;
const { id } = this.props.params;
if (typeof group === 'undefined' || !relationships) {
return (<ColumnIndicator type='loading' />);
@@ -69,18 +92,44 @@ class GroupTimeline extends ImmutablePureComponent {
<TimelineComposeBlock size={46} group={group} shouldCondense={true} autoFocus={false} />
}
<div className='group__feed'>
<StatusListContainer
scrollKey={`group_timeline-${columnId}`}
timelineId={`group:${id}`}
onLoadMore={this.handleLoadMore}
group={group}
withGroupAdmin={relationships && relationships.get('admin')}
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>
</div>
);
<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
alwaysPrepend
scrollKey={`group_timeline-${columnId}`}
timelineId={`group:${id}`}
onLoadMore={this.handleLoadMore}
group={group}
withGroupAdmin={relationships && relationships.get('admin')}
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>
</div>
);
}
}

View File

@@ -59,16 +59,12 @@ export default class Card extends ImmutablePureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func,
};
static defaultProps = {
maxDescription: 50,
compact: false,
};
state = {
@@ -132,37 +128,52 @@ export default class Card extends ImmutablePureComponent {
ref={this.setRef}
className='status-card__image status-card-video'
dangerouslySetInnerHTML={content}
style={{ height }}
style={{
height,
paddingBottom: 0,
}}
/>
);
}
render () {
const { card, maxDescription, compact } = this.props;
const { card } = this.props;
const { width, embedded } = this.state;
if (card === null) {
return null;
}
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const maxDescription = 150;
const cardImg = card.get('image');
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = (card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height');
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
const className = classnames('status-card', {
horizontal,
interactive,
compact: !cardImg && !interactive,
});
const title = interactive ?
<a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'>
<strong>{card.get('title')}</strong>
</a>
: <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const description = (
<div className='status-card__content'>
{title}
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>{provider}</span>
{!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>
<Icon id='link' fixedWidth />
{' '}
{provider}
</span>
</div>
);
let embed = '';
let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
let embed = '';
let thumbnail = card ? <div style={{ backgroundImage: `url(${cardImg})` }} className='status-card__image-image' /> : thumbnail = <div className='status-card__image-image' />;
if (interactive) {
if (embedded) {
@@ -177,7 +188,6 @@ export default class Card extends ImmutablePureComponent {
embed = (
<div className='status-card__image'>
{thumbnail}
<div className='status-card__actions'>
<div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
@@ -191,10 +201,10 @@ export default class Card extends ImmutablePureComponent {
return (
<div className={className} ref={this.setRef}>
{embed}
{!compact && description}
{description}
</div>
);
} else if (card.get('image')) {
} else if (cardImg) {
embed = (
<div className='status-card__image'>
{thumbnail}

View File

@@ -1,3 +1,4 @@
import StatusQuote from '../../../components/status_quote';
import { Link, NavLink } from 'react-router-dom';
import { FormattedDate, FormattedNumber } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
@@ -32,12 +33,17 @@ export default class DetailedStatus extends ImmutablePureComponent {
compact: PropTypes.bool,
showMedia: PropTypes.bool,
onToggleMediaVisibility: PropTypes.func,
onShowRevisions: PropTypes.func,
};
state = {
height: null,
};
handleShowRevisions = () => {
this.props.onShowRevisions(this.props.status);
}
handleOpenVideo = (media, startTime) => {
this.props.onOpenVideo(media, startTime);
}
@@ -110,6 +116,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
blurhash={video.get('blurhash')}
src={video.get('url')}
alt={video.get('description')}
aspectRatio={video.getIn(['meta', 'small', 'aspect'])}
width={300}
height={150}
inline
@@ -185,16 +192,21 @@ export default class DetailedStatus extends ImmutablePureComponent {
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
</NavLink>
{status.get('group') && (
<div className='status__meta'>
Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink>
</div>
)}
{(status.get('group') || status.get('revised_at') !== null) && (
<div className='status__meta'>
{status.get('group') && <React.Fragment>Posted in <NavLink to={`/groups/${status.getIn(['group', 'id'])}`}>{status.getIn(['group', 'title'])}</NavLink></React.Fragment>}
{status.get('revised_at') !== null && <a onClick={this.handleShowRevisions}> Edited</a>}
</div>
)}
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
{media}
{status.get('quote') && <StatusQuote
id={status.get('quote')}
/>}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />

View File

@@ -10,13 +10,16 @@ import './detailed_status_action_bar.scss';
const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
quote: { id: 'status.quote', defaultMessage: 'Quote' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
@@ -50,6 +53,7 @@ class ActionBar extends ImmutablePureComponent {
status: ImmutablePropTypes.map.isRequired,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onQuote: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@@ -79,6 +83,14 @@ class ActionBar extends ImmutablePureComponent {
}
}
handleQuoteClick = (e) => {
if (me) {
this.props.onQuote(this.props.status, e);
} else {
this.props.onOpenUnauthorizedModal();
}
}
handleFavouriteClick = () => {
if (me) {
this.props.onFavourite(this.props.status);
@@ -91,8 +103,8 @@ class ActionBar extends ImmutablePureComponent {
this.props.onDelete(this.props.status, this.context.router.history);
}
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true);
handleEditClick = () => {
this.props.onEdit(this.props.status);
}
handleMentionClick = () => {
@@ -176,7 +188,7 @@ class ActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
@@ -208,17 +220,11 @@ class ActionBar extends ImmutablePureComponent {
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
return (
<div className='detailed-status-action-bar'>
<div className='detailed-status-action-bar__button'>
<IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} />
</div>
<div className='detailed-status-action-bar__button'>
<IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
</div>
<div className='detailed-status-action-bar__button'>
<IconButton className='star-icon' active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
</div>
<div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} title={reblog_disabled ? intl.formatMessage(messages.cannot_quote) : intl.formatMessage(messages.quote)} icon='quote-left' onClick={this.handleQuoteClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
{shareButton}
<div className='detailed-status-action-bar__dropdown'>

View File

@@ -31,8 +31,6 @@ import { showAlertForError } from '../../../actions/alerts';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
@@ -106,14 +104,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}));
},
onDelete (status, history, withRedraft = false) {
onDelete (status, history) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
dispatch(deleteStatus(status.get('id'), history));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history)),
}));
}
},

View File

@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage, injectIntl } from 'react-intl';
import NotificationsCounterIcon from './notifications_counter_icon';
import { me } from '../../../initial_state';
const links = [
<NavLink key='pr1' className='footer-bar__link' to='/home' data-preview-title-id='column.home'>
<i className='tabs-bar__link__icon home'/>
<FormattedMessage id='tabs_bar.home' defaultMessage='Home' />
</NavLink>,
<NavLink key='pr2' className='footer-bar__link' to='/notifications' data-preview-title-id='column.notifications'>
<i className='tabs-bar__link__icon notifications'/>
<NotificationsCounterIcon />
<FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' />
</NavLink>,
<NavLink key='pr3' className='footer-bar__link' to='/groups' data-preview-title-id='column.groups'>
<i className='tabs-bar__link__icon groups'/>
<FormattedMessage id='tabs_bar.groups' defaultMessage='Groups' />
</NavLink>,
<a key='pl4' className='footer-bar__link footer-bar__link--trends' href='https://trends.gab.com' data-preview-title-id='tabs_bar.trends'>
<i className='tabs-bar__link__icon trends'/>
<FormattedMessage id='tabs_bar.trends' defaultMessage='Trends' />
</a>,
]
export default
@injectIntl
@withRouter
class FooterBar extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
}
render () {
const { intl: { formatMessage } } = this.props;
if (!me) return null;
return (
<div className='footer-bar'>
<div className='footer-bar__container'>
{
links.map((link) =>
React.cloneElement(link, {
key: link.props.to,
'aria-label': formatMessage({
id: link.props['data-preview-title-id']
})
}))
}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,66 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../../../initial_state';
import IconButton from '../../../components/icon_button';
import Icon from '../../../components/icon';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const mapStateToProps = state => {
return {
account: state.getIn(['accounts', me]),
};
};
class ProUpgradeModal extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
};
onClickClose = () => {
this.props.onClose('PRO_UPGRADE');
};
render () {
const { intl } = this.props;
return (
<div className='modal-root__modal compose-modal pro-upgrade-modal'>
<div className='compose-modal__header'>
<h3 className='compose-modal__header__title'><FormattedMessage id='promo.gab_pro' defaultMessage='Upgrade to GabPRO' /></h3>
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
</div>
<div className='compose-modal__content pro-upgrade-modal__content'>
<div>
<span className="pro-upgrade-modal__text">
<FormattedMessage id='pro_upgrade_modal.text' defaultMessage='Gab is fully funded by people like you. Please consider supporting us on our mission to defend free expression online for all people.' />
<FormattedMessage id='pro_upgrade_modal.benefits' defaultMessage='Here are just some of the benefits that thousands of GabPRO members receive:' />
</span>
<ul className="pro-upgrade-modal__list">
<li>Schedule Posts</li>
<li>Get Verified</li>
<li>Create Groups</li>
<li>Larger Video and Image Uploads</li>
<li>Receive the PRO Badge</li>
<li>Remove in-feed promotions</li>
<li>More value being added daily!</li>
</ul>
<a href='https://pro.gab.com' className='pro-upgrade-modal__button button'>
<Icon id='arrow-up' fixedWidth/>
<FormattedMessage id='promo.gab_pro' defaultMessage='Upgrade to GabPRO' />
</a>
</div>
</div>
</div>
);
}
}
export default injectIntl(connect(mapStateToProps)(ProUpgradeModal));

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { monthlyExpensesComplete } from '../../../initial_state';
export default class ProgressPanel extends React.PureComponent {
render() {
if (!monthlyExpensesComplete) return null;
const completed = Math.min(monthlyExpensesComplete, 100);
const style = {
width: `${completed}%`,
};
return (
<div className='wtf-panel progress-panel'>
<div className='wtf-panel-header progress-panel-header'>
<div className='wtf-panel-header__label'>Gab's Operational Expenses</div>
</div>
<div className='wtf-panel__content progress-panel__content'>
<span className='progress-panel__text'>We are 100% funded by you.</span>
<div className='progress-panel__bar-container'>
<a className='progress-panel__bar' style={style} href='https://shop.dissenter.com/category/donations'>
<span className='progress-panel__bar__text'>{completed}% covered this month</span>
</a>
</div>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ModalLoading from './modal_loading';
import RelativeTimestamp from '../../../components/relative_timestamp';
export default @injectIntl
class StatusRevisionsList extends ImmutablePureComponent {
static propTypes = {
loading: PropTypes.bool.isRequired,
error: PropTypes.object,
data: PropTypes.array
};
render () {
const { loading, error, data } = this.props;
if (loading || !data) return <ModalLoading />;
if (error) return (
<div className='status-revisions-list'>
<div className='status-revisions-list__error'>
An error occured
</div>
</div>
);
return (
<div className='status-revisions-list'>
{data.map((revision, i) => (
<div key={i} className='status-revisions-list__item'>
<div className='status-revisions-list__item__timestamp'>
<RelativeTimestamp timestamp={revision.created_at} />
</div>
<div className='status-revisions-list__item__text'>{revision.text}</div>
</div>
))}
</div>
);
}
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import IconButton from 'gabsocial/components/icon_button';
import StatusRevisionListContainer from '../containers/status_revision_list_container';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
export default @injectIntl
class StatusRevisionModal extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired,
status: ImmutablePropTypes.map.isRequired
};
render () {
const { intl, onClose, status } = this.props;
return (
<div className='modal-root__modal status-revisions-root'>
<div className='status-revisions'>
<div className='status-revisions__header'>
<h3 className='status-revisions__header__title'><FormattedMessage id='status_revisions.heading' defaultMessage='Revision History' /></h3>
<IconButton className='status-revisions__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={20} />
</div>
<div className='status-revisions__content'>
<StatusRevisionListContainer id={status.get('id')} />
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { load } from '../../../actions/status_revision_list';
import StatusRevisionList from '../components/status_revision_list';
class StatusRevisionListContainer extends ImmutablePureComponent {
componentDidMount() {
this.props.load(this.props.id);
}
render() {
return <StatusRevisionList {...this.props} />;
}
}
const mapStateToProps = state => ({
loading: state.getIn(['status_revision_list', 'loading']),
error: state.getIn(['status_revision_list', 'error']),
data: state.getIn(['status_revision_list', 'data']),
});
const mapDispatchToProps = {
load
};
export default connect(mapStateToProps, mapDispatchToProps)(StatusRevisionListContainer);

View File

@@ -110,6 +110,10 @@ export function MuteModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../../../components/modal');
}
export function StatusRevisionModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/status_revision_modal');
}
export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal');
}