Updated components, style modules

This commit is contained in:
mgabdev 2019-08-13 11:54:29 -04:00
parent ecd081b5ed
commit c58d621daf
57 changed files with 1263 additions and 1205 deletions

View File

@ -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,

View File

@ -1,5 +1,6 @@
import classNames from 'classnames';
import './icon.scss';
export default class Icon extends PureComponent {
static propTypes = {

View File

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

View File

@ -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,

View File

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

View File

@ -24,4 +24,4 @@ export {
ReportModal,
UnauthorizedModal,
VideoModal,
}
};

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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>
&nbsp;·&nbsp;
</span>

View File

@ -0,0 +1 @@
export { default } from './search';

View File

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

View File

@ -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} />

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1 @@
export { default } from './timeline_compose_block';

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
.account-gallery__container {
display: flex;
flex-wrap: wrap;
padding: 4px 2px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
/>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './compose_search';

View File

@ -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,

View File

@ -19,7 +19,7 @@ const messages = defineMessages({
});
@injectIntl
class Option extends PureComponent {
class Option extends ImmutablePureComponent {
static propTypes = {
title: PropTypes.string.isRequired,

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,11 @@
import ColumnIndicator from '../../components/column_indicator';
export default class GenericNotFound extends PureComponent {
render() {
return (
<ColumnIndicator type='missing' />
)
}
}

View File

@ -1,7 +1 @@
import ColumnIndicator from '../../components/column_indicator';
const GenericNotFound = () => (
<ColumnIndicator type='missing' />
);
export default GenericNotFound;
export { default } from './generic_not_found';

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -0,0 +1 @@
export { default } from './list_editor_search';

View File

@ -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}
/>
)
}
}

View File

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

View File

@ -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,

View File

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

View 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>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -0,0 +1 @@
export { default } from './card';

View File

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