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