Merge branch 'develop' of https://code.gab.com/gab/social/gab-social into feature/frontend_refactor
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })}>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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)),
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user