Updated components, style modules
This commit is contained in:
parent
ecd081b5ed
commit
c58d621daf
@ -1,4 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
@ -159,7 +160,7 @@ class DropdownMenu extends PureComponent {
|
||||
|
||||
}
|
||||
|
||||
export default class Dropdown extends PureComponent {
|
||||
export default class Dropdown extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './icon.scss';
|
||||
export default class Icon extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../icon';
|
||||
|
||||
@ -44,7 +44,7 @@ class LoadMore extends PureComponent {
|
||||
onClick={this.handleClick}
|
||||
aria-label={intl.formatMessage(messages.load_more)}
|
||||
>
|
||||
{!gap && <FormattedMessage id='status.load_more' defaultMessage='Load more' />}
|
||||
{!gap && intl.formatMessage(messages.load_more)}
|
||||
{gap && <Icon id='ellipsis-h' />}
|
||||
</button>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { is } from 'immutable';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
@ -15,7 +16,7 @@ const messages = defineMessages({
|
||||
hidden: { id: 'status.media_hidden', defaultMessage: 'Media hidden' },
|
||||
});
|
||||
|
||||
class Item extends PureComponent {
|
||||
class Item extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
attachment: ImmutablePropTypes.map.isRequired,
|
||||
|
@ -1,11 +1,7 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Avatar from '../../avatar';
|
||||
import ComposeFormContainer from '../../../features/compose/containers/compose_form_container';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { cancelReplyCompose } from '../../../actions/compose';
|
||||
import { me } from '../../../initial_state';
|
||||
import ModalLayout from '../modal_layout';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -15,7 +11,6 @@ const messages = defineMessages({
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
composeText: state.getIn(['compose', 'text']),
|
||||
};
|
||||
};
|
||||
@ -25,7 +20,6 @@ export default @connect(mapStateToProps)
|
||||
class ComposeModal extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
composeText: PropTypes.string,
|
||||
@ -49,16 +43,11 @@ class ComposeModal extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, account } = this.props;
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<ModalLayout title={intl.formatMessage(messages.title)} onClose={onClickClose}>
|
||||
<div className='timeline-compose-block'>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Avatar account={account} size={32} />
|
||||
</div>
|
||||
<ComposeFormContainer />
|
||||
</div>
|
||||
<TimelineComposeBlock />
|
||||
</ModalLayout>
|
||||
);
|
||||
}
|
||||
|
@ -24,4 +24,4 @@ export {
|
||||
ReportModal,
|
||||
UnauthorizedModal,
|
||||
VideoModal,
|
||||
}
|
||||
};
|
@ -15,6 +15,7 @@ const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
viewContext: { id: 'lightbox.view_context', defaultMessage: 'View context' },
|
||||
});
|
||||
|
||||
export const previewState = 'previewMediaModal';
|
||||
@ -228,7 +229,9 @@ class MediaModal extends ImmutablePureComponent {
|
||||
|
||||
{status && (
|
||||
<div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>
|
||||
<a href={status.get('url')} onClick={this.handleStatusClick}>
|
||||
{intl.formatMessage(messages.viewContext)}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import ToggleSwitch from '../../toggle_switch';
|
||||
import Button from '../../button';
|
||||
import { closeModal } from '../../../actions/modal';
|
||||
import { muteAccount } from '../../../actions/accounts';
|
||||
import { toggleHideNotifications } from '../../../actions/mutes';
|
||||
|
||||
const messages = defineMessages({
|
||||
muteMessage: { id: 'confirmations.mute.message', defaultMessage: 'Are you sure you want to mute {name}?' },
|
||||
hideNotifications: { id: 'mute_modal.hide_notifications', defaultMessage: 'Hide notifications from this user?' },
|
||||
cancel: { id: 'confirmation_modal.cancel', defaultMessage: 'Cancel' },
|
||||
confirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
@ -72,15 +78,11 @@ class MuteModal extends PureComponent {
|
||||
<div className='modal-root__modal mute-modal'>
|
||||
<div className='mute-modal__container'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='confirmations.mute.message'
|
||||
defaultMessage='Are you sure you want to mute {name}?'
|
||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||
/>
|
||||
{intl.formatMessage(messages.muteMessage, { name: <strong>@{account.get('acct')}</strong> })}
|
||||
</p>
|
||||
<div>
|
||||
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||
{intl.formatMessage(messages.hideNotifications)}
|
||||
{' '}
|
||||
<ToggleSwitch id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
||||
</label>
|
||||
@ -89,10 +91,10 @@ class MuteModal extends PureComponent {
|
||||
|
||||
<div className='mute-modal__action-bar'>
|
||||
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
|
||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||
{intl.formatMessage(messages.cancel)}
|
||||
</Button>
|
||||
<Button onClick={this.handleClick} ref={this.setRef}>
|
||||
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
|
||||
{intl.formatMessage(messages.confirm)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { OrderedSet } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ToggleSwitch from '../../toggle_switch';
|
||||
@ -14,6 +14,10 @@ const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
||||
hint: { id: 'report.hint', defaultMessage: 'The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' },
|
||||
forwardHint: { id: 'report.forward_hint', defaultMessage: 'The account is from another server. Send an anonymized copy of the report there as well?' },
|
||||
forward: { id: 'report.forward', defaultMessage: 'Forward to {target}' },
|
||||
target: { id: 'report.target', defaultMessage: 'Report {target}' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -89,12 +93,14 @@ class ReportModal extends ImmutablePureComponent {
|
||||
<div className='modal-root__modal report-modal'>
|
||||
<div className='report-modal__target'>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
|
||||
<FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: <strong>{account.get('acct')}</strong> }} />
|
||||
{intl.formatMessage(messages.target, {
|
||||
target: <strong>{account.get('acct')}</strong>
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className='report-modal__container'>
|
||||
<div className='report-modal__comment'>
|
||||
<p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p>
|
||||
<p>{intl.formatMessage(messages.hint)}</p>
|
||||
|
||||
<textarea
|
||||
className='setting-text light'
|
||||
@ -108,11 +114,15 @@ class ReportModal extends ImmutablePureComponent {
|
||||
|
||||
{domain && (
|
||||
<div>
|
||||
<p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
|
||||
<p>{intl.formatMessage(messages.forwardHint)}</p>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<ToggleSwitch id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
|
||||
<label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label>
|
||||
<label htmlFor='report-forward' className='setting-toggle__label'>
|
||||
{intl.formatMessage(messages.forward, {
|
||||
target: domain
|
||||
})}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -2,7 +2,6 @@ import Base from '../modal_base';
|
||||
import Bundle from '../../features/ui/util/bundle';
|
||||
import BundleModalError from '../bundle_modal_error';
|
||||
import {
|
||||
ModalLoading,
|
||||
ActionsModal,
|
||||
MediaModal,
|
||||
VideoModal,
|
||||
@ -14,6 +13,8 @@ import {
|
||||
UnauthorizedModal,
|
||||
} from '../modal';
|
||||
|
||||
import ModalLoading from '../modal_loading';
|
||||
|
||||
import {
|
||||
MuteModal,
|
||||
ReportModal,
|
||||
|
@ -18,6 +18,8 @@ const mapStateToProps = (state, { pollId }) => ({
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
vote: { id: 'poll.vote', defaultMessage: 'Vote' },
|
||||
refresh: { id: 'poll.refresh', defaultMessage: 'Refresh' },
|
||||
});
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
@ -149,14 +151,14 @@ class Poll extends ImmutablePureComponent {
|
||||
{
|
||||
!showResults &&
|
||||
<Button className='poll__button' disabled={disabled} onClick={this.handleVote} secondary>
|
||||
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
|
||||
{intl.formatMessage(messages.vote)}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
showResults && !this.props.disabled &&
|
||||
<span>
|
||||
<button className='poll__link' onClick={this.handleRefresh}>
|
||||
<FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
|
||||
{intl.formatMessage(messages.refresh)}
|
||||
</button>
|
||||
·
|
||||
</span>
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './search';
|
@ -1,7 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Icon from '../icon';
|
||||
import SearchPopout from '../search_popout';
|
||||
|
||||
import './search.scss';
|
||||
|
||||
export default class Search extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
@ -11,44 +14,22 @@ export default class Search extends PureComponent {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
openInRoute: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
searchTitle: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
withOverlay: PropTypes.bool,
|
||||
handleClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleClear = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.value.length > 0 || this.props.submitted) {
|
||||
this.props.onClear();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
this.props.onShow();
|
||||
@ -59,32 +40,46 @@ export default class Search extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, value, submitted } = this.props;
|
||||
const { value, submitted, placeholder, className, searchTitle, onKeyUp, handleClear, handleSubmit, withOverlay, onChange } = this.props;
|
||||
const { expanded } = this.state;
|
||||
|
||||
const hasValue = value.length > 0 || submitted;
|
||||
const classes = classNames('search', className);
|
||||
const iconClass = hasValue ? 'active' : '';
|
||||
|
||||
return (
|
||||
<div className='search'>
|
||||
<div className={classes}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
|
||||
<span className='invisible'>{placeholder}</span>
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onChange={onChange}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
</label>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' className={hasValue ? '' : 'active'} />
|
||||
<Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={handleClear}>
|
||||
<Icon id='search' className={iconClass} />
|
||||
<Icon id='times-circle' className={iconClass} aria-label={placeholder} />
|
||||
</div>
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this}>
|
||||
<SearchPopout />
|
||||
</Overlay>
|
||||
|
||||
{
|
||||
withOverlay &&
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this}>
|
||||
<SearchPopout />
|
||||
</Overlay>
|
||||
}
|
||||
|
||||
{
|
||||
(searchTitle && handleSubmit) &&
|
||||
<Button onClick={handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
|
||||
}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -12,15 +12,16 @@ class SectionHeadlineBarItem extends PureComponent {
|
||||
PropTypes.string,
|
||||
PropTypes.node,
|
||||
]),
|
||||
exact: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { to, title, icon, className, onClick } = this.props;
|
||||
const { to, title, icon, className, onClick, exact } = this.props;
|
||||
|
||||
const classes = classNames('section-header-bar__item', className);
|
||||
|
||||
if (to) {
|
||||
return (<NavLink className={classes} to={to}>{title}</NavLink>);
|
||||
return (<NavLink className={classes} exact={exact} to={to}>{title}</NavLink>);
|
||||
} else if (icon) {
|
||||
<button className={classes} onClick={onClick} title={title}>
|
||||
<Icon id={icon} fixedWidth />
|
||||
@ -33,14 +34,17 @@ class SectionHeadlineBarItem extends PureComponent {
|
||||
|
||||
export default class SectionHeadlineBar extends PureComponent {
|
||||
static propTypes = {
|
||||
items: PropTypes.array,
|
||||
items: PropTypes.array.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { items } = this.props;
|
||||
const { items, className } = this.props;
|
||||
|
||||
const classes = classNames('section-headline-bar', className);
|
||||
|
||||
return (
|
||||
<div className='section-headline-bar'>
|
||||
<div className={classes}>
|
||||
{
|
||||
items.forEach(item, i => (
|
||||
<SectionHeadlineBarItem key={`shbi-{i}`} {...item} />
|
||||
|
@ -1,9 +1,10 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ToggleSwitch from '../toggle_switch';
|
||||
|
||||
import './setting_toggle.scss';
|
||||
|
||||
export default class SettingToggle extends PureComponent {
|
||||
export default class SettingToggle extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
prefix: PropTypes.string,
|
||||
|
@ -10,7 +10,7 @@ import RelativeTimestamp from '../relative_timestamp';
|
||||
import DisplayName from '../display_name';
|
||||
import StatusContent from '../status_content/status_content';
|
||||
import StatusActionBar from '../status_action_bar/status_action_bar';
|
||||
import Card from '../../features/status/components/card';
|
||||
import Card from '../../features/status/components/card/card';
|
||||
import { MediaGallery, Video } from '../../features/ui/util/async-components';
|
||||
import Icon from '../icon';
|
||||
import Poll from '../../components/poll';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { Set as ImmutableSet } from 'immutable';
|
||||
import noop from 'lodash/noop';
|
||||
import StatusContent from '../status_content';
|
||||
@ -21,7 +22,7 @@ const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
class StatusCheckBox extends PureComponent {
|
||||
class StatusCheckBox extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classnames from 'classnames';
|
||||
import { isRtl } from '../../utils/rtl';
|
||||
@ -9,7 +10,7 @@ import './status_content.scss';
|
||||
|
||||
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
|
||||
|
||||
export default class StatusContent extends PureComponent {
|
||||
export default class StatusContent extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './timeline_compose_block';
|
@ -0,0 +1,40 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../avatar';
|
||||
import ComposeFormContainer from '../../features/compose/containers/compose_form_container';
|
||||
import { me } from '../../initial_state';
|
||||
|
||||
import './timeline_compose_block.scss';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class TimelineComposeBlock extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
size: PropTypes.number,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
size: 32,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, size, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<div className='timeline-compose-block'>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Avatar account={account} size={size} />
|
||||
</div>
|
||||
<ComposeFormContainer {...rest} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
.timeline-compose-block {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
@include gab-container-standards();
|
||||
|
||||
.emoji-picker-wrapper {
|
||||
.emoji-picker-dropdown {
|
||||
top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.compose-form {
|
||||
flex: 1 1;
|
||||
padding: 0 0 0 20px !important;
|
||||
position: relative;
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchAccountByUsername,
|
||||
} from '../../actions/accounts';
|
||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import { getAccountGallery } from '../../selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import { openModal } from '../../actions/modal';
|
||||
import { me } from '../../initial_state';
|
||||
import SectionHeadlineBar from '../../components/section_headline_bar';
|
||||
|
||||
import './account_gallery.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
posts: { id: 'account.posts', defaultMessage: 'Gabs' },
|
||||
postsWithReplies: { id: 'account.posts_with_replies', defaultMessage: 'Gabs and replies' },
|
||||
media: { id: 'account.media', defaultMessage: 'Media' },
|
||||
error: { id: 'empty_column.account_unavailable', defaultMessage: 'Profile unavailable' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() == username.toLowerCase());
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
}
|
||||
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const isLocked = state.getIn(['accounts', accountId, 'locked'], false);
|
||||
const isFollowing = state.getIn(['relationships', accountId, 'following'], false);
|
||||
const unavailable = (me === accountId) ? false : (isBlocked || (isLocked && !isFollowing));
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
attachments: getAccountGallery(state, accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
|
||||
};
|
||||
};
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
maxId: PropTypes.string,
|
||||
onLoadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleLoadMore = () => {
|
||||
this.props.onLoadMore(this.props.maxId);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<LoadMore
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.handleLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(accountId));
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId)) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
if (this.props.hasMore) {
|
||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (150 > offset && !this.props.isLoading) {
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
|
||||
}
|
||||
};
|
||||
|
||||
handleLoadOlder = e => {
|
||||
e.preventDefault();
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
|
||||
handleOpenMedia = attachment => {
|
||||
if (attachment.get('type') === 'video') {
|
||||
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
|
||||
} else {
|
||||
const media = attachment.getIn(['status', 'media_attachments']);
|
||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
||||
|
||||
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
|
||||
}
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
if (c) {
|
||||
this.setState({ width: c.offsetWidth });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { attachments, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
} else if (accountId === -1 || (!attachments && isLoading)) {
|
||||
return ( <ColumnIndicator type='loading' /> );
|
||||
} else if (unavailable) {
|
||||
return (<ColumnIndicator type='error' message={intl.formatMessage(messages.error)} />);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable-list scrollable-list--flex' onScroll={this.handleScroll}>
|
||||
<SectionHeadlineBar
|
||||
className='account-section-headline'
|
||||
items={[
|
||||
{
|
||||
exact: true,
|
||||
to: `/${accountUsername}`,
|
||||
title: intl.formatMessage(messages.posts),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
to: `/${accountUsername}/with_replies`,
|
||||
title: intl.formatMessage(messages.postsWithReplies),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
to: `/${accountUsername}/media`,
|
||||
title: intl.formatMessage(messages.media),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{
|
||||
attachments.size == 0 &&
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='slist__append'>
|
||||
<ColumnIndicator type='loading' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.account-gallery__container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 4px 2px;
|
||||
}
|
@ -1,216 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchAccountByUsername,
|
||||
} from 'gabsocial/actions/accounts';
|
||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { getAccountGallery } from 'gabsocial/selectors';
|
||||
import MediaItem from './components/media_item';
|
||||
import LoadMore from 'gabsocial/components/load_more';
|
||||
import { openModal } from 'gabsocial/actions/modal';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() == username.toLowerCase());
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
}
|
||||
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const isLocked = state.getIn(['accounts', accountId, 'locked'], false);
|
||||
const isFollowing = state.getIn(['relationships', accountId, 'following'], false);
|
||||
const unavailable = (me === accountId) ? false : (isBlocked || (isLocked && !isFollowing));
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
attachments: getAccountGallery(state, accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
|
||||
};
|
||||
};
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
maxId: PropTypes.string,
|
||||
onLoadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleLoadMore = () => {
|
||||
this.props.onLoadMore(this.props.maxId);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<LoadMore
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.handleLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(accountId));
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId)) {
|
||||
this.props.dispatch(fetchAccount(nextProps.params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
if (this.props.hasMore) {
|
||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (150 > offset && !this.props.isLoading) {
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
|
||||
}
|
||||
};
|
||||
|
||||
handleLoadOlder = e => {
|
||||
e.preventDefault();
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
|
||||
handleOpenMedia = attachment => {
|
||||
if (attachment.get('type') === 'video') {
|
||||
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
|
||||
} else {
|
||||
const media = attachment.getIn(['status', 'media_attachments']);
|
||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
||||
|
||||
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') }));
|
||||
}
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
if (c) {
|
||||
this.setState({ width: c.offsetWidth });
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { attachments, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
} else if (accountId === -1 || (!attachments && isLoading)) {
|
||||
return ( <ColumnIndicator type='loading' /> );
|
||||
} else if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable-list scrollable-list--flex' onScroll={this.handleScroll}>
|
||||
<div className='account__section-headline'>
|
||||
<div style={{ width: '100%', display: 'flex' }}>
|
||||
<NavLink exact to={`/${accountUsername}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Gabs' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/${accountUsername}/with_replies`}>
|
||||
<FormattedMessage id='account.posts_with_replies' defaultMessage='Gabs and replies' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/${accountUsername}/media`}>
|
||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{
|
||||
attachments.size == 0 &&
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='slist__append'>
|
||||
<ColumnIndicator type='loading' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './account_gallery';
|
@ -0,0 +1,155 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
||||
import StatusList from '../../components/status_list/status_list';
|
||||
import ColumnIndicator from '../../components/column_indicator/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
||||
import { me } from '../../initial_state';
|
||||
import SectionHeadlineBar from '../../components/section_headline_bar' ;
|
||||
|
||||
const messages = defineMessages({
|
||||
posts: { id: 'account.posts', defaultMessage: 'Gabs' },
|
||||
postsWithReplies: { id: 'account.posts_with_replies', defaultMessage: 'Gabs and replies' },
|
||||
media: { id: 'account.media', defaultMessage: 'Media' },
|
||||
error: { id: 'empty_column.account_unavailable', defaultMessage: 'Profile unavailable' },
|
||||
});
|
||||
|
||||
const emptyList = ImmutableList();
|
||||
|
||||
const mapStateToProps = (state, { params: { username }, withReplies = false }) => {
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() == username.toLowerCase());
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
}
|
||||
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const isLocked = state.getIn(['accounts', accountId, 'locked'], false);
|
||||
const isFollowing = state.getIn(['relationships', accountId, 'following'], false);
|
||||
const unavailable = (me == accountId) ? false : (isBlocked || (isLocked && !isFollowing));
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
featuredStatusIds: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
withReplies: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { username }, accountId, withReplies } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
|
||||
if (!withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||
this.props.dispatch(fetchAccount(nextProps.accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(nextProps.accountId));
|
||||
|
||||
if (!nextProps.withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.accountId));
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.accountId, { withReplies: nextProps.withReplies }));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername, intl } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
} else if (accountId === -1 || (!statusIds && isLoading)) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
} else if (unavailable) {
|
||||
return (<ColumnIndicator type='error' message={intl.formatMessage(messages.error)} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<SectionHeadlineBar
|
||||
className='account-section-headline'
|
||||
items={[
|
||||
{
|
||||
exact: true,
|
||||
to: `/${accountUsername}`,
|
||||
title: intl.formatMessage(messages.posts),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
to: `/${accountUsername}/with_replies`,
|
||||
title: intl.formatMessage(messages.postsWithReplies),
|
||||
},
|
||||
{
|
||||
exact: true,
|
||||
to: `/${accountUsername}/media`,
|
||||
title: intl.formatMessage(messages.media),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<StatusList
|
||||
scrollKey='account_timeline'
|
||||
statusIds={statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No gabs here!' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,147 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
||||
import StatusList from '../../components/status_list/status_list';
|
||||
import ColumnIndicator from '../../components/column_indicator/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
const emptyList = ImmutableList();
|
||||
|
||||
const mapStateToProps = (state, { params: { username }, withReplies = false }) => {
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() == username.toLowerCase());
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
}
|
||||
|
||||
const path = withReplies ? `${accountId}:with_replies` : accountId;
|
||||
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const isLocked = state.getIn(['accounts', accountId, 'locked'], false);
|
||||
const isFollowing = state.getIn(['relationships', accountId, 'following'], false);
|
||||
const unavailable = (me == accountId) ? false : (isBlocked || (isLocked && !isFollowing));
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list,
|
||||
featuredStatusIds: ImmutablePropTypes.list,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
withReplies: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { username }, accountId, withReplies } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(accountId));
|
||||
|
||||
if (!withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(accountId));
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.accountId && nextProps.accountId !== -1 && (nextProps.accountId !== this.props.accountId && nextProps.accountId) || nextProps.withReplies !== this.props.withReplies) {
|
||||
this.props.dispatch(fetchAccount(nextProps.accountId));
|
||||
this.props.dispatch(fetchAccountIdentityProofs(nextProps.accountId));
|
||||
|
||||
if (!nextProps.withReplies) {
|
||||
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.accountId));
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(nextProps.accountId, { withReplies: nextProps.withReplies }));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
} else if (accountId === -1 || (!statusIds && isLoading)) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<div className='account__section-headline'>
|
||||
<div style={{ width: '100%', display: 'flex' }}>
|
||||
<NavLink exact to={`/${accountUsername}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Gabs' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/${accountUsername}/with_replies`}>
|
||||
<FormattedMessage id='account.posts_with_replies' defaultMessage='Gabs and replies' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/${accountUsername}/media`}>
|
||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
<StatusList
|
||||
scrollKey='account_timeline'
|
||||
statusIds={statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No gabs here!' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './account_timeline';
|
@ -0,0 +1,121 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import StatusListContainer from '../../containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnSettings from './components/column_settings';
|
||||
import HomeColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
expandCommunityTimeline,
|
||||
expandPublicTimeline,
|
||||
} from '../../actions/timelines';
|
||||
import {
|
||||
connectCommunityStream,
|
||||
connectPublicStream,
|
||||
} from '../../actions/streaming';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Community timeline' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const allFediverse = state.getIn(['settings', 'community', 'other', 'allFediverse']);
|
||||
const onlyMedia = state.getIn(['settings', 'community', 'other', 'onlyMedia']);
|
||||
|
||||
const timelineId = allFediverse ? 'public' : 'community';
|
||||
|
||||
return {
|
||||
timelineId,
|
||||
allFediverse,
|
||||
onlyMedia,
|
||||
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class CommunityTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onlyMedia: false,
|
||||
allFediverse: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
allFediverse: PropTypes.bool,
|
||||
timelineId: PropTypes.string,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.allFediverse !== this.props.allFediverse) {
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
this.disconnect();
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, onlyMedia, timelineId, allFediverse } = this.props;
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.title)}>
|
||||
<HomeColumnHeader activeItem='all' active={hasUnread} >
|
||||
<ColumnSettings />
|
||||
</HomeColumnHeader>
|
||||
<StatusListContainer
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The community timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,121 +1 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import StatusListContainer from '../../containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnSettings from './components/column_settings';
|
||||
import HomeColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
expandCommunityTimeline,
|
||||
expandPublicTimeline,
|
||||
} from '../../actions/timelines';
|
||||
import {
|
||||
connectCommunityStream,
|
||||
connectPublicStream,
|
||||
} from '../../actions/streaming';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Community timeline' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const allFediverse = state.getIn(['settings', 'community', 'other', 'allFediverse']);
|
||||
const onlyMedia = state.getIn(['settings', 'community', 'other', 'onlyMedia']);
|
||||
|
||||
const timelineId = allFediverse ? 'public' : 'community';
|
||||
|
||||
return {
|
||||
timelineId,
|
||||
allFediverse,
|
||||
onlyMedia,
|
||||
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class CommunityTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onlyMedia: false,
|
||||
allFediverse: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
allFediverse: PropTypes.bool,
|
||||
timelineId: PropTypes.string,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.allFediverse !== this.props.allFediverse) {
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
this.disconnect();
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia, allFediverse } = this.props;
|
||||
|
||||
if (allFediverse) {
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
else {
|
||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, onlyMedia, timelineId, allFediverse } = this.props;
|
||||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.title)}>
|
||||
<HomeColumnHeader activeItem='all' active={hasUnread} >
|
||||
<ColumnSettings />
|
||||
</HomeColumnHeader>
|
||||
<StatusListContainer
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The community timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './community_timeline';
|
@ -0,0 +1,69 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Search from '../../../../components/search';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class ComposeSearch extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
openInRoute: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleClear = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.value.length > 0 || this.props.submitted) {
|
||||
this.props.onClear();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, value, onShow, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<Search
|
||||
value={value}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
handleClear={this.handleClear}
|
||||
onShow={onShow}
|
||||
withOverlay
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './compose_search';
|
@ -1,6 +1,7 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../../ui/util/async-components';
|
||||
@ -154,7 +155,7 @@ class ModifierPicker extends PureComponent {
|
||||
}
|
||||
|
||||
@injectIntl
|
||||
class EmojiPickerMenu extends PureComponent {
|
||||
class EmojiPickerMenu extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
custom_emojis: ImmutablePropTypes.list,
|
||||
@ -291,7 +292,7 @@ class EmojiPickerMenu extends PureComponent {
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class EmojiPickerDropdown extends PureComponent {
|
||||
class EmojiPickerDropdown extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
custom_emojis: ImmutablePropTypes.list,
|
||||
|
@ -19,7 +19,7 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
@injectIntl
|
||||
class Option extends PureComponent {
|
||||
class Option extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
|
@ -1,98 +0,0 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Icon from '../../../components/icon';
|
||||
import SearchPopout from '../../../components/search_popout';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Search extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func.isRequired,
|
||||
openInRoute: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
expanded: false,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleClear = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.props.value.length > 0 || this.props.submitted) {
|
||||
this.props.onClear();
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onSubmit();
|
||||
|
||||
if (this.props.openInRoute) {
|
||||
this.context.router.history.push('/search');
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus = () => {
|
||||
this.setState({ expanded: true });
|
||||
this.props.onShow();
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
this.setState({ expanded: false });
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, value, submitted } = this.props;
|
||||
const { expanded } = this.state;
|
||||
const hasValue = value.length > 0 || submitted;
|
||||
|
||||
return (
|
||||
<div className='search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span>
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
/>
|
||||
</label>
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' className={hasValue ? '' : 'active'} />
|
||||
<Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
</div>
|
||||
<Overlay show={expanded && !hasValue} placement='bottom' target={this}>
|
||||
<SearchPopout />
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
@ -11,7 +11,6 @@ import SearchResultsContainer from './containers/search_results_container';
|
||||
import { changeComposing } from '../../actions/compose';
|
||||
import elephantUIPlane from '../../../images/logo_ui_column_footer.png';
|
||||
import { mascot } from '../../initial_state';
|
||||
import Icon from '../../components/icon';
|
||||
|
||||
import './compose.scss';
|
||||
|
||||
@ -33,7 +32,7 @@ const mapStateToProps = (state, ownProps) => ({
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Compose extends PureComponent {
|
||||
class Compose extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
submitSearch,
|
||||
showSearch,
|
||||
} from '../../../actions/search';
|
||||
import Search from '../components/search';
|
||||
import ComposeSearch from '../components/compose_search';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['search', 'value']),
|
||||
@ -31,4 +31,4 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Search);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ComposeSearch);
|
||||
|
@ -0,0 +1,63 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import AccountAuthorize from './components/account_authorize';
|
||||
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class FollowRequests extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFollowRequests());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowRequests());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='user-plus' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountAuthorize key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,63 +1 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import AccountAuthorize from './components/account_authorize';
|
||||
import { fetchFollowRequests, expandFollowRequests } from '../../actions/accounts';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class FollowRequests extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFollowRequests());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFollowRequests());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='user-plus' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountAuthorize key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './follow_requests';
|
@ -0,0 +1,11 @@
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
|
||||
export default class GenericNotFound extends PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ColumnIndicator type='missing' />
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1 @@
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
|
||||
const GenericNotFound = () => (
|
||||
<ColumnIndicator type='missing' />
|
||||
);
|
||||
|
||||
export default GenericNotFound;
|
||||
export { default } from './generic_not_found';
|
@ -1,9 +1,8 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { changeValue, submit, setUp } from '../../../actions/group_editor';
|
||||
import Icon from '../../../components/icon';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import Column from '../../../components/column';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -32,7 +31,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class Edit extends PureComponent {
|
||||
class Edit extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -1,16 +1,13 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import StatusListContainer from '../../../containers/status_list_container';
|
||||
import Column from '../../../components/column';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { connectGroupStream } from '../../../actions/streaming';
|
||||
import { expandGroupTimeline } from '../../../actions/timelines';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import ComposeFormContainer from '../../../../gabsocial/features/compose/containers/compose_form_container';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import TimelineComposeBlock from '../../../components/timeline_compose_block';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: state.getIn(['accounts', me]),
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
relationships: state.getIn(['group_relationships', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `group:${props.params.id}`, 'unread']) > 0,
|
||||
@ -18,7 +15,7 @@ const mapStateToProps = (state, props) => ({
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class GroupTimeline extends PureComponent {
|
||||
class GroupTimeline extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
@ -31,7 +28,6 @@ class GroupTimeline extends PureComponent {
|
||||
hasUnread: PropTypes.bool,
|
||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
relationships: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@ -57,7 +53,7 @@ class GroupTimeline extends PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { columnId, group, relationships, account } = this.props;
|
||||
const { columnId, group, relationships } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
if (typeof group === 'undefined' || !relationships) {
|
||||
@ -67,17 +63,13 @@ class GroupTimeline extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{relationships.get('member') && (
|
||||
<div className='timeline-compose-block'>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Avatar account={account} size={46} />
|
||||
</div>
|
||||
<ComposeFormContainer group={group} shouldCondense autoFocus={false} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{
|
||||
relationships.get('member') &&
|
||||
<TimelineComposeBlock size={46} group={group} shouldCondense={true} autoFocus={false} />
|
||||
}
|
||||
|
||||
<div className='group__feed'>
|
||||
<div className='group__feed'>
|
||||
<StatusListContainer
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
timelineId={`group:${id}`}
|
||||
@ -85,7 +77,7 @@ class GroupTimeline extends PureComponent {
|
||||
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>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { changeSetting, saveSettings } from '../../../../actions/settings';
|
||||
import SettingToggle from '../../../../components/setting_toggle';
|
||||
@ -18,7 +19,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
class ColumnSettings extends PureComponent {
|
||||
class ColumnSettings extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './list_editor_search';
|
@ -1,8 +1,6 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
import Icon from '../../../components/icon';
|
||||
import Button from '../../../components/button';
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../../actions/lists';
|
||||
import Search from '../../../../components/search';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
||||
@ -21,7 +19,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class Search extends PureComponent {
|
||||
class ListEditorSearch extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
@ -50,31 +48,21 @@ class Search extends PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
const { value, intl, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
<Button onClick={this.handleSubmit}>{intl.formatMessage(messages.searchTitle)}</Button>
|
||||
</div>
|
||||
);
|
||||
<Search
|
||||
className='list-editor-search'
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
handleSubmit={this.handleSubmit}
|
||||
handleClear={this.handleClear}
|
||||
searchTitle={intl.formatMessage(messages.searchTitle)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -3,7 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Account from './components/account';
|
||||
import Search from './components/search';
|
||||
import ListEditorSearch from './components/list_editor_search';
|
||||
import EditListForm from './components/edit_list_form/edit_list_form';
|
||||
import ColumnSubheading from '../../components/column_subheading';
|
||||
import IconButton from '../../components/icon_button';
|
||||
@ -85,7 +85,7 @@ class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.addToList)} />
|
||||
<Search />
|
||||
<ListEditorSearch />
|
||||
<div className='list-editor__accounts'>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
|
@ -1,10 +1,11 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ColumnHeaderSettingButton from '../../../../components/column_header_setting_button';
|
||||
import SettingToggle from '../../../../components/setting_toggle';
|
||||
import ColumnSettingsHeading from '../../../../components/column_settings_heading';
|
||||
|
||||
export default class ColumnSettings extends PureComponent {
|
||||
export default class ColumnSettings extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
|
@ -1,187 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
expandNotifications,
|
||||
scrollTopNotifications,
|
||||
dequeueNotifications,
|
||||
} from '../../actions/notifications';
|
||||
import NotificationContainer from './containers/notification_container';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import FilterBarContainer from './containers/filter_bar_container';
|
||||
import { createSelector } from 'reselect';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
});
|
||||
|
||||
const getNotifications = createSelector([
|
||||
state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
|
||||
state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
|
||||
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
|
||||
state => state.getIn(['notifications', 'items']),
|
||||
], (showFilterBar, allowedType, excludedTypes, notifications) => {
|
||||
if (!showFilterBar || allowedType === 'all') {
|
||||
// used if user changed the notification settings after loading the notifications from the server
|
||||
// otherwise a list of notifications will come pre-filtered from the backend
|
||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||
}
|
||||
return notifications.filter(item => item !== null && allowedType === item.get('type'));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
|
||||
notifications: getNotifications(state),
|
||||
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
||||
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Notifications extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
notifications: ImmutablePropTypes.list.isRequired,
|
||||
showFilterBar: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
isUnread: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
dequeueNotifications: PropTypes.func,
|
||||
totalQueuedNotificationsCount: PropTypes.number,
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
this.handleLoadOlder.cancel();
|
||||
this.handleScrollToTop.cancel();
|
||||
this.handleScroll.cancel();
|
||||
this.props.dispatch(scrollTopNotifications(false));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleDequeueNotifications();
|
||||
this.props.dispatch(scrollTopNotifications(true));
|
||||
}
|
||||
|
||||
handleLoadGap = (maxId) => {
|
||||
this.props.dispatch(expandNotifications({ maxId }));
|
||||
};
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
const last = this.props.notifications.last();
|
||||
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
|
||||
}, 300, { leading: true });
|
||||
|
||||
handleScrollToTop = debounce(() => {
|
||||
this.props.dispatch(scrollTopNotifications(true));
|
||||
}, 100);
|
||||
|
||||
handleScroll = debounce(() => {
|
||||
this.props.dispatch(scrollTopNotifications(false));
|
||||
}, 100);
|
||||
|
||||
setColumnRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.column.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleDequeueNotifications = () => {
|
||||
this.props.dispatch(dequeueNotifications());
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, notifications, isLoading, isUnread, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
|
||||
|
||||
let scrollableContent = null;
|
||||
|
||||
const filterBarContainer = showFilterBar
|
||||
? (<FilterBarContainer />)
|
||||
: null;
|
||||
|
||||
if (isLoading && this.scrollableContent) {
|
||||
scrollableContent = this.scrollableContent;
|
||||
} else if (notifications.size > 0 || hasMore) {
|
||||
scrollableContent = notifications.map((item, index) => item === null ? (
|
||||
<LoadMore
|
||||
gap
|
||||
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||
onClick={this.handleLoadGap}
|
||||
/>
|
||||
) : (
|
||||
<NotificationContainer
|
||||
key={item.get('id')}
|
||||
notification={item}
|
||||
accountId={item.get('account')}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
scrollableContent = null;
|
||||
}
|
||||
|
||||
this.scrollableContent = scrollableContent;
|
||||
|
||||
const scrollContainer = (
|
||||
<ScrollableList
|
||||
scrollKey='notifications'
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notifications.size === 0}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />}
|
||||
onLoadMore={this.handleLoadOlder}
|
||||
onScrollToTop={this.handleScrollToTop}
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column ref={this.setColumnRef} heading={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader icon='bell' active={isUnread} title={intl.formatMessage(messages.title)}>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
{filterBarContainer}
|
||||
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
|
||||
{scrollContainer}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './notifications';
|
188
app/javascript/gabsocial/features/notifications/notifications.js
Normal file
188
app/javascript/gabsocial/features/notifications/notifications.js
Normal file
@ -0,0 +1,188 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
expandNotifications,
|
||||
scrollTopNotifications,
|
||||
dequeueNotifications,
|
||||
} from '../../actions/notifications';
|
||||
import NotificationContainer from './containers/notification_container';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import FilterBarContainer from './containers/filter_bar_container';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
});
|
||||
|
||||
const getNotifications = createSelector([
|
||||
state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
|
||||
state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
|
||||
state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
|
||||
state => state.getIn(['notifications', 'items']),
|
||||
], (showFilterBar, allowedType, excludedTypes, notifications) => {
|
||||
if (!showFilterBar || allowedType === 'all') {
|
||||
// used if user changed the notification settings after loading the notifications from the server
|
||||
// otherwise a list of notifications will come pre-filtered from the backend
|
||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||
}
|
||||
return notifications.filter(item => item !== null && allowedType === item.get('type'));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
showFilterBar: state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
|
||||
notifications: getNotifications(state),
|
||||
isLoading: state.getIn(['notifications', 'isLoading'], true),
|
||||
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Notifications extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
notifications: ImmutablePropTypes.list.isRequired,
|
||||
showFilterBar: PropTypes.bool.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
isUnread: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
dequeueNotifications: PropTypes.func,
|
||||
totalQueuedNotificationsCount: PropTypes.number,
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
this.handleLoadOlder.cancel();
|
||||
this.handleScrollToTop.cancel();
|
||||
this.handleScroll.cancel();
|
||||
this.props.dispatch(scrollTopNotifications(false));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleDequeueNotifications();
|
||||
this.props.dispatch(scrollTopNotifications(true));
|
||||
}
|
||||
|
||||
handleLoadGap = (maxId) => {
|
||||
this.props.dispatch(expandNotifications({ maxId }));
|
||||
};
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
const last = this.props.notifications.last();
|
||||
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
|
||||
}, 300, { leading: true });
|
||||
|
||||
handleScrollToTop = debounce(() => {
|
||||
this.props.dispatch(scrollTopNotifications(true));
|
||||
}, 100);
|
||||
|
||||
handleScroll = debounce(() => {
|
||||
this.props.dispatch(scrollTopNotifications(false));
|
||||
}, 100);
|
||||
|
||||
setColumnRef = c => {
|
||||
this.column = c;
|
||||
}
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.column.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleDequeueNotifications = () => {
|
||||
this.props.dispatch(dequeueNotifications());
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, notifications, isLoading, isUnread, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
|
||||
|
||||
let scrollableContent = null;
|
||||
|
||||
const filterBarContainer = showFilterBar
|
||||
? (<FilterBarContainer />)
|
||||
: null;
|
||||
|
||||
if (isLoading && this.scrollableContent) {
|
||||
scrollableContent = this.scrollableContent;
|
||||
} else if (notifications.size > 0 || hasMore) {
|
||||
scrollableContent = notifications.map((item, index) => item === null ? (
|
||||
<LoadMore
|
||||
gap
|
||||
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||
onClick={this.handleLoadGap}
|
||||
/>
|
||||
) : (
|
||||
<NotificationContainer
|
||||
key={item.get('id')}
|
||||
notification={item}
|
||||
accountId={item.get('account')}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
scrollableContent = null;
|
||||
}
|
||||
|
||||
this.scrollableContent = scrollableContent;
|
||||
|
||||
const scrollContainer = (
|
||||
<ScrollableList
|
||||
scrollKey='notifications'
|
||||
isLoading={isLoading}
|
||||
showLoading={isLoading && notifications.size === 0}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />}
|
||||
onLoadMore={this.handleLoadOlder}
|
||||
onScrollToTop={this.handleScrollToTop}
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
{scrollableContent}
|
||||
</ScrollableList>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column ref={this.setColumnRef} heading={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader icon='bell' active={isUnread} title={intl.formatMessage(messages.title)}>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
{filterBarContainer}
|
||||
<TimelineQueueButtonHeader onClick={this.handleDequeueNotifications} count={totalQueuedNotificationsCount} itemType='notification' />
|
||||
{scrollContainer}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import ComposeFormContainer from '../../compose/containers/compose_form_container';
|
||||
import NotificationsContainer from '../../../containers/notifications_container';
|
||||
import LoadingBarContainer from '../../../containers/loading_bar_container';
|
||||
import ModalContainer from '../../../containers/modal_container';
|
||||
|
||||
export default class Compose extends PureComponent {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ComposeFormContainer />
|
||||
<NotificationsContainer />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +1 @@
|
||||
import ComposeFormContainer from '../../compose/containers/compose_form_container';
|
||||
import NotificationsContainer from '../../../containers/notifications_container';
|
||||
import LoadingBarContainer from '../../../containers/loading_bar_container';
|
||||
import ModalContainer from '../../../containers/modal_container';
|
||||
|
||||
export default class Compose extends PureComponent {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<ComposeFormContainer />
|
||||
<NotificationsContainer />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './compose';
|
@ -0,0 +1,82 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { expandHashtagTimeline } from 'gabsocial/actions/timelines';
|
||||
import Masonry from 'react-masonry-infinite';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import DetailedStatusContainer from 'gabsocial/features/status/containers/detailed_status_container';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
|
||||
const mapStateToProps = (state, { hashtag }) => ({
|
||||
statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false),
|
||||
hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class HashtagTimeline extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
hashtag: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, hashtag } = this.props;
|
||||
|
||||
dispatch(expandHashtagTimeline(hashtag));
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const maxId = this.props.statusIds.last();
|
||||
|
||||
if (maxId) {
|
||||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.masonry = c;
|
||||
}
|
||||
|
||||
handleHeightChange = debounce(() => {
|
||||
if (!this.masonry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.masonry.forcePack();
|
||||
}, 50)
|
||||
|
||||
render () {
|
||||
const { statusIds, hasMore, isLoading } = this.props;
|
||||
|
||||
const sizes = [
|
||||
{ columns: 1, gutter: 0 },
|
||||
{ mq: '415px', columns: 1, gutter: 10 },
|
||||
{ mq: '640px', columns: 2, gutter: 10 },
|
||||
{ mq: '960px', columns: 3, gutter: 10 },
|
||||
{ mq: '1255px', columns: 3, gutter: 10 },
|
||||
];
|
||||
|
||||
const loader = (isLoading && statusIds.isEmpty()) ? <ColumnIndicator type='loading' key={0} /> : undefined;
|
||||
|
||||
return (
|
||||
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
|
||||
{statusIds.map(statusId => (
|
||||
<div className='statuses-grid__item' key={statusId}>
|
||||
<DetailedStatusContainer
|
||||
id={statusId}
|
||||
compact
|
||||
measureHeight
|
||||
onHeightChange={this.handleHeightChange}
|
||||
/>
|
||||
</div>
|
||||
)).toArray()}
|
||||
</Masonry>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { expandHashtagTimeline } from 'gabsocial/actions/timelines';
|
||||
import Masonry from 'react-masonry-infinite';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import DetailedStatusContainer from 'gabsocial/features/status/containers/detailed_status_container';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
|
||||
const mapStateToProps = (state, { hashtag }) => ({
|
||||
statusIds: state.getIn(['timelines', `hashtag:${hashtag}`, 'items'], ImmutableList()),
|
||||
isLoading: state.getIn(['timelines', `hashtag:${hashtag}`, 'isLoading'], false),
|
||||
hasMore: state.getIn(['timelines', `hashtag:${hashtag}`, 'hasMore'], false),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class HashtagTimeline extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
hashtag: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch, hashtag } = this.props;
|
||||
|
||||
dispatch(expandHashtagTimeline(hashtag));
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const maxId = this.props.statusIds.last();
|
||||
|
||||
if (maxId) {
|
||||
this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId }));
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.masonry = c;
|
||||
}
|
||||
|
||||
handleHeightChange = debounce(() => {
|
||||
if (!this.masonry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.masonry.forcePack();
|
||||
}, 50)
|
||||
|
||||
render () {
|
||||
const { statusIds, hasMore, isLoading } = this.props;
|
||||
|
||||
const sizes = [
|
||||
{ columns: 1, gutter: 0 },
|
||||
{ mq: '415px', columns: 1, gutter: 10 },
|
||||
{ mq: '640px', columns: 2, gutter: 10 },
|
||||
{ mq: '960px', columns: 3, gutter: 10 },
|
||||
{ mq: '1255px', columns: 3, gutter: 10 },
|
||||
];
|
||||
|
||||
const loader = (isLoading && statusIds.isEmpty()) ? <ColumnIndicator type='loading' key={0} /> : undefined;
|
||||
|
||||
return (
|
||||
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
|
||||
{statusIds.map(statusId => (
|
||||
<div className='statuses-grid__item' key={statusId}>
|
||||
<DetailedStatusContainer
|
||||
id={statusId}
|
||||
compact
|
||||
measureHeight
|
||||
onHeightChange={this.handleHeightChange}
|
||||
/>
|
||||
</div>
|
||||
)).toArray()}
|
||||
</Masonry>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './hashtag_timeline';
|
@ -1,96 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { expandPublicTimeline, expandCommunityTimeline } from 'gabsocial/actions/timelines';
|
||||
import Masonry from 'react-masonry-infinite';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import DetailedStatusContainer from 'gabsocial/features/status/containers/detailed_status_container';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
|
||||
const mapStateToProps = (state, { local }) => {
|
||||
const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
|
||||
|
||||
return {
|
||||
statusIds: timeline.get('items', ImmutableList()),
|
||||
isLoading: timeline.get('isLoading', false),
|
||||
hasMore: timeline.get('hasMore', false),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class PublicTimeline extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
local: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this._connect();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.local !== this.props.local) {
|
||||
this._connect();
|
||||
}
|
||||
}
|
||||
|
||||
_connect () {
|
||||
const { dispatch, local } = this.props;
|
||||
|
||||
dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { dispatch, statusIds, local } = this.props;
|
||||
const maxId = statusIds.last();
|
||||
|
||||
if (maxId) {
|
||||
dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.masonry = c;
|
||||
}
|
||||
|
||||
handleHeightChange = debounce(() => {
|
||||
if (!this.masonry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.masonry.forcePack();
|
||||
}, 50)
|
||||
|
||||
render () {
|
||||
const { statusIds, hasMore, isLoading } = this.props;
|
||||
|
||||
const sizes = [
|
||||
{ columns: 1, gutter: 0 },
|
||||
{ mq: '415px', columns: 1, gutter: 10 },
|
||||
{ mq: '640px', columns: 2, gutter: 10 },
|
||||
{ mq: '960px', columns: 3, gutter: 10 },
|
||||
{ mq: '1255px', columns: 3, gutter: 10 },
|
||||
];
|
||||
|
||||
const loader = (isLoading && statusIds.isEmpty()) ? <ColumnIndicator type='loading' key={0} /> : undefined;
|
||||
|
||||
return (
|
||||
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
|
||||
{statusIds.map(statusId => (
|
||||
<div className='statuses-grid__item' key={statusId}>
|
||||
<DetailedStatusContainer
|
||||
id={statusId}
|
||||
compact
|
||||
measureHeight
|
||||
onHeightChange={this.handleHeightChange}
|
||||
/>
|
||||
</div>
|
||||
)).toArray()}
|
||||
</Masonry>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './public_timeline';
|
@ -0,0 +1,97 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { expandPublicTimeline, expandCommunityTimeline } from 'gabsocial/actions/timelines';
|
||||
import Masonry from 'react-masonry-infinite';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import DetailedStatusContainer from 'gabsocial/features/status/containers/detailed_status_container';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
|
||||
const mapStateToProps = (state, { local }) => {
|
||||
const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
|
||||
|
||||
return {
|
||||
statusIds: timeline.get('items', ImmutableList()),
|
||||
isLoading: timeline.get('isLoading', false),
|
||||
hasMore: timeline.get('hasMore', false),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class PublicTimeline extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
local: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this._connect();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.local !== this.props.local) {
|
||||
this._connect();
|
||||
}
|
||||
}
|
||||
|
||||
_connect () {
|
||||
const { dispatch, local } = this.props;
|
||||
|
||||
dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { dispatch, statusIds, local } = this.props;
|
||||
const maxId = statusIds.last();
|
||||
|
||||
if (maxId) {
|
||||
dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.masonry = c;
|
||||
}
|
||||
|
||||
handleHeightChange = debounce(() => {
|
||||
if (!this.masonry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.masonry.forcePack();
|
||||
}, 50)
|
||||
|
||||
render () {
|
||||
const { statusIds, hasMore, isLoading } = this.props;
|
||||
|
||||
const sizes = [
|
||||
{ columns: 1, gutter: 0 },
|
||||
{ mq: '415px', columns: 1, gutter: 10 },
|
||||
{ mq: '640px', columns: 2, gutter: 10 },
|
||||
{ mq: '960px', columns: 3, gutter: 10 },
|
||||
{ mq: '1255px', columns: 3, gutter: 10 },
|
||||
];
|
||||
|
||||
const loader = (isLoading && statusIds.isEmpty()) ? <ColumnIndicator type='loading' key={0} /> : undefined;
|
||||
|
||||
return (
|
||||
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
|
||||
{statusIds.map(statusId => (
|
||||
<div className='statuses-grid__item' key={statusId}>
|
||||
<DetailedStatusContainer
|
||||
id={statusId}
|
||||
compact
|
||||
measureHeight
|
||||
onHeightChange={this.handleHeightChange}
|
||||
/>
|
||||
</div>
|
||||
)).toArray()}
|
||||
</Masonry>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import Immutable from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import punycode from 'punycode';
|
||||
import classnames from 'classnames';
|
||||
import Icon from '../../../components/icon';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
import './card.scss';
|
||||
|
||||
const IDNA_PREFIX = 'xn--';
|
||||
|
||||
@ -52,7 +55,7 @@ const addAutoPlay = html => {
|
||||
return html;
|
||||
};
|
||||
|
||||
export default class Card extends PureComponent {
|
||||
export default class Card extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
card: ImmutablePropTypes.map,
|
@ -0,0 +1 @@
|
||||
export { default } from './card';
|
@ -1,23 +1,15 @@
|
||||
import { Fragment } from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import WhoToFollowPanel from '../components/panel';
|
||||
import LinkFooter from '../components/link_footer';
|
||||
import PromoPanel from '../components/promo_panel';
|
||||
import UserPanel from '../components/user_panel';
|
||||
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
|
||||
import Avatar from '../components/avatar';
|
||||
import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||
import { me } from '../initial_state';
|
||||
import ColumnsArea from '../components/columns_area';
|
||||
import TimelineComposeBlock from '../components/timeline_compose_block';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
account: state.getIn(['accounts', me]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class HomePage extends ImmutablePureComponent {
|
||||
export default class HomePage extends PureComponent {
|
||||
render () {
|
||||
const {children, account} = this.props;
|
||||
const {children} = this.props;
|
||||
|
||||
return (
|
||||
<ColumnsArea
|
||||
@ -26,7 +18,7 @@ class HomePage extends ImmutablePureComponent {
|
||||
RIGHT: (
|
||||
<Fragment>
|
||||
<GroupSidebarPanel />
|
||||
{/*<WhoToFollowPanel />*/}
|
||||
{ /* <WhoToFollowPanel /> */ }
|
||||
</Fragment>
|
||||
),
|
||||
LEFT: (
|
||||
@ -38,13 +30,7 @@ class HomePage extends ImmutablePureComponent {
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className='timeline-compose-block'>
|
||||
<div className='timeline-compose-block__avatar'>
|
||||
<Avatar account={account} size={46} />
|
||||
</div>
|
||||
<ComposeFormContainer shouldCondense={true} autoFocus={false} />
|
||||
</div>
|
||||
|
||||
<TimelineComposeBlock size={46} shouldCondense={true} autoFocus={false} />
|
||||
{children}
|
||||
</ColumnsArea>
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user