Large update for all components
reorganization, linting, updating file imports, consolidation warning: there will be errors in this commit todo: update webpack, add missing styles, scss files, consolidate the rest of components within features/*
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'gabsocial/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
|
||||
@@ -4,13 +4,12 @@ import {
|
||||
fetchAccountByUsername,
|
||||
} from 'gabsocial/actions/accounts';
|
||||
import { expandAccountMediaTimeline } from '../../actions/timelines';
|
||||
import LoadingIndicator from 'gabsocial/components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
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 MissingIndicator from 'gabsocial/components/missing_indicator';
|
||||
import { openModal } from 'gabsocial/actions/modal';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
@@ -24,8 +23,7 @@ const mapStateToProps = (state, { params: { username }, withReplies = false }) =
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
@@ -34,7 +32,7 @@ const mapStateToProps = (state, { params: { username }, withReplies = false }) =
|
||||
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));
|
||||
const unavailable = (me === accountId) ? false : (isBlocked || (isLocked && !isFollowing));
|
||||
|
||||
return {
|
||||
accountId,
|
||||
@@ -66,6 +64,7 @@ class LoadMoreMedia extends ImmutablePureComponent {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@@ -91,8 +90,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(accountId));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
@@ -152,17 +150,15 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
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>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId == -1 || (!attachments && isLoading)) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
@@ -173,21 +169,11 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<div className='slist slist--flex' onScroll={this.handleScroll}>
|
||||
<div className='scrollable-list scrollable-list--flex' onScroll={this.handleScroll}>
|
||||
<div className='account__section-headline'>
|
||||
<div style={{width: '100%', display: 'flex'}}>
|
||||
<div style={{ width: '100%', display: 'flex' }}>
|
||||
<NavLink exact to={`/${accountUsername}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Gabs' />
|
||||
</NavLink>
|
||||
@@ -210,7 +196,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
{
|
||||
attachments.size == 0 &&
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.'/>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -219,7 +205,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='slist__append'>
|
||||
<LoadingIndicator />
|
||||
<ColumnIndicator type='loading' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import InnerHeader from '../../account/components/header';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import InnerHeader from './inner_header';
|
||||
import MovedNote from './moved_note';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export default class Header extends ImmutablePureComponent {
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Button from 'gabsocial/components/button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { autoPlayGif, me, isStaff } from 'gabsocial/initial_state';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Avatar from 'gabsocial/components/avatar';
|
||||
import { shortNumberFormat } from 'gabsocial/utils/numbers';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import DropdownMenuContainer from 'gabsocial/containers/dropdown_menu_container';
|
||||
import ProfileInfoPanel from '../../ui/components/profile_info_panel';
|
||||
import { debounce } from 'lodash';
|
||||
import Button from '../../../components/button';
|
||||
import { autoPlayGif, me, isStaff } from '../../../initial_state';
|
||||
import Icon from '../../../components/icon';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import { shortNumberFormat } from '../../../utils/numbers';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import ProfileInfoPanel from './profile_info_panel';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
@@ -310,7 +308,7 @@ class Header extends ImmutablePureComponent {
|
||||
<div className='account__header__extra__buttons'>
|
||||
{actionBtn}
|
||||
{account.get('id') !== me &&
|
||||
<Button className='button button-alternative-2' onClick={this.props.onMention}>
|
||||
<Button className='button button--alternative-2' onClick={this.props.onMention}>
|
||||
<FormattedMessage id='account.mention' defaultMessage='Mention' values={{
|
||||
name: account.get('acct')
|
||||
}} />
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Button from 'gabsocial/components/button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import VerificationBadge from 'gabsocial/components/verification_badge';
|
||||
import ProBadge from 'gabsocial/components/pro_badge';
|
||||
import DonorBadge from 'gabsocial/components/donor_badge';
|
||||
import InvestorBadge from 'gabsocial/components/investor_badge';
|
||||
import VerifiedIcon from 'gabsocial/components/verified_icon';
|
||||
import Badge from 'gabsocial/components/badge';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -25,6 +22,16 @@ const dateFormatOptions = {
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, { account }) => {
|
||||
const identity_proofs = account ? state.getIn(['identity_proofs', account.get('id')], ImmutableList()) : ImmutableList();
|
||||
return {
|
||||
identity_proofs,
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ProfileInfoPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -67,16 +74,16 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||
<div className='profile-info-panel-content__name'>
|
||||
<h1>
|
||||
<span dangerouslySetInnerHTML={displayNameHtml} />
|
||||
{account.get('is_verified') && <VerificationBadge />}
|
||||
{account.get('is_verified') && <VerifiedIcon />}
|
||||
{badge}
|
||||
<small>@{acct} {lockedIcon}</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className='profile-info-panel-content__badges'>
|
||||
{account.get('is_pro') && <ProBadge />}
|
||||
{account.get('is_donor') && <DonorBadge />}
|
||||
{account.get('is_investor') && <InvestorBadge />}
|
||||
{account.get('is_pro') && <Badge type='pro' />}
|
||||
{account.get('is_donor') && <Badge type='donor' />}
|
||||
{account.get('is_investor') && <Badge type='investor' />}
|
||||
<div className='profile-info-panel-content__badges__join-date'>
|
||||
<Icon id="calendar"/>
|
||||
<FormattedMessage id='account.member_since' defaultMessage='Member since {date}' values={{
|
||||
@@ -124,18 +131,4 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, { account }) => {
|
||||
const identity_proofs = account ? state.getIn(['identity_proofs', account.get('id')], ImmutableList()) : ImmutableList();
|
||||
return {
|
||||
identity_proofs,
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
};
|
||||
};
|
||||
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, null, null, {
|
||||
forwardRef: true,
|
||||
}
|
||||
)(ProfileInfoPanel))
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import Header from '../components/header';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
@@ -17,9 +17,9 @@ import { initMuteModal } from '../../../actions/mutes';
|
||||
import { initReport } from '../../../actions/reports';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { unfollowModal } from '../../../initial_state';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import Header from '../components/header';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
|
||||
import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines';
|
||||
import StatusList from '../../components/status_list';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
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 MissingIndicator from 'gabsocial/components/missing_indicator';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
@@ -22,8 +21,7 @@ const mapStateToProps = (state, { params: { username }, withReplies = false }) =
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
@@ -75,8 +73,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
this.props.dispatch(expandAccountTimeline(accountId, { withReplies }));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
@@ -104,19 +101,9 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId == -1 || (!statusIds && isLoading)) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
} else if (accountId === -1 || (!statusIds && isLoading)) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
@@ -132,7 +119,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
return (
|
||||
<Column>
|
||||
<div className='account__section-headline'>
|
||||
<div style={{width: '100%', display: 'flex'}}>
|
||||
<div style={{ width: '100%', display: 'flex' }}>
|
||||
<NavLink exact to={`/${accountUsername}`}>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Gabs' />
|
||||
</NavLink>
|
||||
|
||||
65
app/javascript/gabsocial/features/blocks/blocks.js
Normal file
65
app/javascript/gabsocial/features/blocks/blocks.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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 AccountContainer from '../../containers/account_container';
|
||||
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Blocks extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchBlocks());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandBlocks());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
|
||||
|
||||
return (
|
||||
<Column icon='ban' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +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 LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import { fetchBlocks, expandBlocks } from '../../actions/blocks';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'blocks', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Blocks extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchBlocks());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandBlocks());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
|
||||
|
||||
return (
|
||||
<Column icon='ban' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<ScrollableList
|
||||
scrollKey='blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './blocks';
|
||||
@@ -1,8 +1,22 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
import SettingToggle from '../../../components/setting_toggle';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
|
||||
export default @injectIntl
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'community']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onChange(key, checked) {
|
||||
dispatch(changeSetting(['community', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ColumnSettings extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -17,8 +31,18 @@ class ColumnSettings extends PureComponent {
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
|
||||
<SettingToggle settings={settings} settingPath={['other', 'allFediverse']} onChange={onChange} label={<FormattedMessage id='community.column_settings.all_fediverse' defaultMessage='All Fediverse' />} />
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['other', 'onlyMedia']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />}
|
||||
/>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['other', 'allFediverse']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='community.column_settings.all_fediverse' defaultMessage='All Fediverse' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'community']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['community', ...key], checked));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import StatusListContainer from '../../containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import HomeColumnHeader from '../../components/home_column_header';
|
||||
import ColumnSettings from './components/column_settings';
|
||||
import HomeColumnHeader from '../../components/column_header';
|
||||
import {
|
||||
expandCommunityTimeline,
|
||||
expandPublicTimeline,
|
||||
@@ -104,9 +104,9 @@ class CommunityTimeline extends PureComponent {
|
||||
const { intl, hasUnread, onlyMedia, timelineId, allFediverse } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<Column heading={intl.formatMessage(messages.title)}>
|
||||
<HomeColumnHeader activeItem='all' active={hasUnread} >
|
||||
<ColumnSettingsContainer />
|
||||
<ColumnSettings />
|
||||
</HomeColumnHeader>
|
||||
<StatusListContainer
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { meUsername } from 'gabsocial/initial_state';
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Avatar from '../../../components/avatar/avatar';
|
||||
import DisplayName from '../../../components/display_name/display_name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
export default class AutosuggestAccount extends ImmutablePureComponent {
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
account: getAccount(state, id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps)
|
||||
class AutosuggestAccount extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
@@ -14,7 +26,9 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='autosuggest-account' title={account.get('acct')}>
|
||||
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
|
||||
<div className='autosuggest-account-icon'>
|
||||
<Avatar account={account} size={18} />
|
||||
</div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import CharacterCounter from './character_counter';
|
||||
import Button from '../../../components/button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { length } from 'stringz';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import classNames from 'classnames';
|
||||
import CharacterCounter from './character_counter';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
import UploadFormContainer from '../containers/upload_form_container';
|
||||
import UploadForm from './upload_form';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
import { isMobile } from '../../../utils/is_mobile';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { length } from 'stringz';
|
||||
import { countableText } from '../util/counter';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
import Button from '../../../components/button';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
const maxPostCharacterCount = 3000;
|
||||
@@ -104,10 +103,10 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
|
||||
if (this.props.text !== this.autosuggestTextarea.textbox.value) {
|
||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||
// Update the state to match the current text
|
||||
this.props.onChange(this.autosuggestTextarea.textarea.value);
|
||||
this.props.onChange(this.autosuggestTextarea.textbox.value);
|
||||
}
|
||||
|
||||
// Submit disabled:
|
||||
@@ -171,8 +170,8 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
selectionStart = selectionEnd;
|
||||
}
|
||||
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textbox.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,9 +188,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
handleEmojiPick = (data) => {
|
||||
const { text } = this.props;
|
||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||
const { text } = this.props;
|
||||
const position = this.autosuggestTextarea.textbox.selectionStart;
|
||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||
|
||||
this.props.onPickEmoji(position, data, needsSpace);
|
||||
}
|
||||
@@ -215,7 +214,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
const composeClassNames = classNames({
|
||||
'compose-form': true,
|
||||
'condensed': condensed,
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={composeClassNames} ref={this.setForm} onClick={this.handleClick}>
|
||||
@@ -224,7 +223,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
{ !shouldCondense && <ReplyIndicatorContainer /> }
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
|
||||
<AutosuggestInput
|
||||
<AutosuggestTextbox
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
@@ -245,7 +244,8 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
</div>
|
||||
|
||||
<AutosuggestTextarea
|
||||
<AutosuggestTextbox
|
||||
textarea={true}
|
||||
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
@@ -263,11 +263,11 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
{
|
||||
!condensed &&
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<UploadForm />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
}
|
||||
</AutosuggestTextarea>
|
||||
</AutosuggestTextbox>
|
||||
|
||||
{
|
||||
!condensed &&
|
||||
|
||||
@@ -4,7 +4,7 @@ import Overlay from 'react-overlays/lib/Overlay';
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import { buildCustomEmojis } from '../../emoji/emoji';
|
||||
import { buildCustomEmojis } from '../../../components/emoji/emoji';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
|
||||
@@ -5,8 +5,16 @@ import Permalink from '../../../components/permalink';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me } from '../../../initial_state';
|
||||
|
||||
export default class NavigationBar extends ImmutablePureComponent {
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class NavigationBar extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
@@ -26,7 +34,9 @@ export default class NavigationBar extends ImmutablePureComponent {
|
||||
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
|
||||
</Permalink>
|
||||
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'>
|
||||
<FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import IconButton from 'gabsocial/components/icon_button';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import AutosuggestInput from 'gabsocial/components/autosuggest_input';
|
||||
import classNames from 'classnames';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Icon from '../../../components/icon';
|
||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox';
|
||||
|
||||
const messages = defineMessages({
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
||||
@@ -75,7 +73,7 @@ class Option extends PureComponent {
|
||||
tabIndex='0'
|
||||
/>
|
||||
|
||||
<AutosuggestInput
|
||||
<AutosuggestTextbox
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={25}
|
||||
value={title}
|
||||
@@ -143,7 +141,7 @@ class PollForm extends ImmutablePureComponent {
|
||||
|
||||
<div className='poll__footer'>
|
||||
{options.size < 4 && (
|
||||
<button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
|
||||
<button className='button button--secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
|
||||
)}
|
||||
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import { searchEnabled } from '../../../initial_state';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
|
||||
@@ -3,10 +3,9 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Hashtag from '../../../components/hashtag';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import WhoToFollowPanel from '../../ui/components/who_to_follow_panel';
|
||||
// import TrendsPanel from '../../ui/components/trends_panel';
|
||||
import TrendingItem from '../../../components/trending_item';
|
||||
import Icon from '../../../components/icon';
|
||||
import WhoToFollowPanel from '../../../components/panel';
|
||||
|
||||
export default @injectIntl
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
@@ -64,7 +63,7 @@ class SearchResults extends ImmutablePureComponent {
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||
|
||||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
{results.get('hashtags').map(hashtag => <TrendingItem key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UploadProgress from './upload_progress';
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
|
||||
export default class UploadForm extends ImmutablePureComponent {
|
||||
const mapStateToProps = state => ({
|
||||
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
mediaIds: ImmutablePropTypes.list.isRequired,
|
||||
@@ -15,7 +20,7 @@ export default class UploadForm extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-wrapper'>
|
||||
<UploadProgressContainer />
|
||||
<UploadProgress />
|
||||
|
||||
<div className='compose-form__uploads-wrapper'>
|
||||
{mediaIds.map(id => (
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
export default class UploadProgress extends PureComponent {
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'is_uploading']),
|
||||
progress: state.getIn(['compose', 'progress']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class UploadProgress extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import AutosuggestAccount from '../components/autosuggest_account';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
account: getAccount(state, id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default connect(makeMapStateToProps)(AutosuggestAccount);
|
||||
@@ -1,11 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import NavigationBar from '../components/navigation_bar';
|
||||
import { me } from '../../../initial_state';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(NavigationBar);
|
||||
@@ -1,7 +0,0 @@
|
||||
import UploadForm from '../components/upload_form';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(UploadForm);
|
||||
@@ -1,8 +0,0 @@
|
||||
import UploadProgress from '../components/upload_progress';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'is_uploading']),
|
||||
progress: state.getIn(['compose', 'progress']),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(UploadProgress);
|
||||
@@ -1,17 +1,17 @@
|
||||
import ComposeFormContainer from './containers/compose_form_container';
|
||||
import NavigationContainer from './containers/navigation_container';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { mountCompose, unmountCompose } from '../../actions/compose';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import SearchContainer from './containers/search_container';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
import ComposeFormContainer from './containers/compose_form_container';
|
||||
import NavigationBar from './components/navigation_bar';
|
||||
import { mountCompose, unmountCompose } from '../../actions/compose';
|
||||
import SearchContainer from './containers/search_container';
|
||||
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 'gabsocial/components/icon';
|
||||
import Icon from '../../components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
@@ -102,7 +102,7 @@ class Compose extends PureComponent {
|
||||
|
||||
<div className='drawer__pager'>
|
||||
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<NavigationBar onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
|
||||
export default class Conversation extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
conversationId: PropTypes.string.isRequired,
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
lastStatusId: PropTypes.string,
|
||||
unread:PropTypes.bool.isRequired,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
markRead: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (!this.context.router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lastStatusId, unread, markRead } = this.props;
|
||||
|
||||
if (unread) {
|
||||
markRead();
|
||||
}
|
||||
|
||||
// : TODO :
|
||||
this.context.router.history.push(`/statuses/${lastStatusId}`);
|
||||
}
|
||||
|
||||
handleHotkeyMoveUp = () => {
|
||||
this.props.onMoveUp(this.props.conversationId);
|
||||
}
|
||||
|
||||
handleHotkeyMoveDown = () => {
|
||||
this.props.onMoveDown(this.props.conversationId);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, lastStatusId, unread } = this.props;
|
||||
|
||||
if (lastStatusId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StatusContainer
|
||||
id={lastStatusId}
|
||||
unread={unread}
|
||||
otherAccounts={accounts}
|
||||
onMoveUp={this.handleHotkeyMoveUp}
|
||||
onMoveDown={this.handleHotkeyMoveDown}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ConversationContainer from '../containers/conversation_container';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export default class ConversationsList extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
conversations: ImmutablePropTypes.list.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func,
|
||||
};
|
||||
|
||||
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||
this._selectChild(elementIndex, true);
|
||||
}
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||
this._selectChild(elementIndex, false);
|
||||
}
|
||||
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.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();
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
const last = this.props.conversations.last();
|
||||
|
||||
if (last && last.get('last_status')) {
|
||||
this.props.onLoadMore(last.get('last_status'));
|
||||
}
|
||||
}, 300, { leading: true })
|
||||
|
||||
render () {
|
||||
const { conversations, onLoadMore, ...other } = this.props;
|
||||
|
||||
return (
|
||||
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
|
||||
{conversations.map(item => (
|
||||
<ConversationContainer
|
||||
key={item.get('id')}
|
||||
conversationId={item.get('id')}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import Conversation from '../components/conversation';
|
||||
import { markConversationRead } from '../../../actions/conversations';
|
||||
|
||||
const mapStateToProps = (state, { conversationId }) => {
|
||||
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
||||
|
||||
return {
|
||||
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
||||
unread: conversation.get('unread'),
|
||||
lastStatusId: conversation.get('last_status', null),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { conversationId }) => ({
|
||||
markRead: () => dispatch(markConversationRead(conversationId)),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
|
||||
@@ -1,14 +0,0 @@
|
||||
import ConversationsList from '../components/conversations_list';
|
||||
import { expandConversations } from '../../../actions/conversations';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
conversations: state.getIn(['conversations', 'items']),
|
||||
isLoading: state.getIn(['conversations', 'isLoading'], true),
|
||||
hasMore: state.getIn(['conversations', 'hasMore'], false),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);
|
||||
@@ -1,60 +0,0 @@
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connectDirectStream } from '../../actions/streaming';
|
||||
import ConversationsListContainer from './containers/conversations_list_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class DirectTimeline extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(mountConversations());
|
||||
dispatch(expandConversations());
|
||||
this.disconnect = dispatch(connectDirectStream());
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(unmountConversations());
|
||||
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandConversations({ maxId }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader icon='envelope' active={hasUnread} title={intl.formatMessage(messages.title)} />
|
||||
|
||||
<ConversationsListContainer
|
||||
scrollKey='direct_timeline'
|
||||
timelineId='direct'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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 DomainContainer from '../../containers/domain_container';
|
||||
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
domains: state.getIn(['domain_lists', 'blocks', 'items']),
|
||||
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Blocks extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
domains: ImmutablePropTypes.orderedSet,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.dispatch(fetchDomainBlocks());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandDomainBlocks());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render() {
|
||||
const { intl, domains, hasMore } = this.props;
|
||||
|
||||
if (!domains) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
|
||||
|
||||
return (
|
||||
<Column icon='minus-circle' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='domain_blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{domains.map(domain =>
|
||||
<DomainContainer key={domain} domain={domain} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +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 LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import DomainContainer from '../../containers/domain_container';
|
||||
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
domains: state.getIn(['domain_lists', 'blocks', 'items']),
|
||||
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Blocks extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
domains: ImmutablePropTypes.orderedSet,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchDomainBlocks());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandDomainBlocks());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, domains, hasMore } = this.props;
|
||||
|
||||
if (!domains) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
|
||||
|
||||
return (
|
||||
<Column icon='minus-circle' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<ScrollableList
|
||||
scrollKey='domain_blocks'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{domains.map(domain =>
|
||||
<DomainContainer key={domain} domain={domain} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './domain_blocks';
|
||||
@@ -1,82 +0,0 @@
|
||||
import emojify from '../emoji';
|
||||
|
||||
describe('emoji', () => {
|
||||
describe('.emojify', () => {
|
||||
it('ignores unknown shortcodes', () => {
|
||||
expect(emojify(':foobarbazfake:')).toEqual(':foobarbazfake:');
|
||||
});
|
||||
|
||||
it('ignores shortcodes inside of tags', () => {
|
||||
expect(emojify('<p data-foo=":smile:"></p>')).toEqual('<p data-foo=":smile:"></p>');
|
||||
});
|
||||
|
||||
it('works with unclosed tags', () => {
|
||||
expect(emojify('hello>')).toEqual('hello>');
|
||||
expect(emojify('<hello')).toEqual('<hello');
|
||||
});
|
||||
|
||||
it('works with unclosed shortcodes', () => {
|
||||
expect(emojify('smile:')).toEqual('smile:');
|
||||
expect(emojify(':smile')).toEqual(':smile');
|
||||
});
|
||||
|
||||
it('does unicode', () => {
|
||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
|
||||
expect(emojify('👨👩👧👧')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
|
||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
|
||||
expect(emojify('\u2757')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
});
|
||||
|
||||
it('does multiple unicode', () => {
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
|
||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
||||
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
|
||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
||||
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
|
||||
});
|
||||
|
||||
it('ignores unicode inside of tags', () => {
|
||||
expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).toEqual('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>');
|
||||
});
|
||||
|
||||
it('does multiple emoji properly (issue 5188)', () => {
|
||||
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
|
||||
});
|
||||
|
||||
it('does an emoji that has no shortcode', () => {
|
||||
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg" />');
|
||||
});
|
||||
|
||||
it('does an emoji whose filename is irregular', () => {
|
||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
||||
});
|
||||
|
||||
it('avoid emojifying on invisible text', () => {
|
||||
expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'))
|
||||
.toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>');
|
||||
expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } }))
|
||||
.toEqual('<span class="invisible">:luigi:</span>');
|
||||
});
|
||||
|
||||
it('avoid emojifying on invisible text with nested tags', () => {
|
||||
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
|
||||
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||
});
|
||||
|
||||
it('skips the textual presentation VS15 character', () => {
|
||||
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
|
||||
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734.svg" />');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,176 +0,0 @@
|
||||
import { pick } from 'lodash';
|
||||
import { emojiIndex } from 'emoji-mart';
|
||||
import { search } from '../emoji_mart_search_light';
|
||||
|
||||
const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']);
|
||||
|
||||
describe('emoji_index', () => {
|
||||
it('should give same result for emoji_index_light and emoji-mart', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'pineapple',
|
||||
unified: '1f34d',
|
||||
native: '🍍',
|
||||
},
|
||||
];
|
||||
expect(search('pineapple').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('orders search results correctly', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'apple',
|
||||
unified: '1f34e',
|
||||
native: '🍎',
|
||||
},
|
||||
{
|
||||
id: 'pineapple',
|
||||
unified: '1f34d',
|
||||
native: '🍍',
|
||||
},
|
||||
{
|
||||
id: 'green_apple',
|
||||
unified: '1f34f',
|
||||
native: '🍏',
|
||||
},
|
||||
{
|
||||
id: 'iphone',
|
||||
unified: '1f4f1',
|
||||
native: '📱',
|
||||
},
|
||||
];
|
||||
expect(search('apple').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('can include/exclude categories', () => {
|
||||
expect(search('flag', { include: ['people'] })).toEqual([]);
|
||||
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
|
||||
});
|
||||
|
||||
it('(different behavior from emoji-mart) do not erases custom emoji if not passed again', () => {
|
||||
const custom = [
|
||||
{
|
||||
id: 'gabsocial',
|
||||
name: 'gabsocial',
|
||||
short_names: ['gabsocial'],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['gabsocial'],
|
||||
imageUrl: 'http://example.com',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
search('', { custom });
|
||||
emojiIndex.search('', { custom });
|
||||
const expected = [];
|
||||
const lightExpected = [
|
||||
{
|
||||
id: 'gabsocial',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
expect(search('masto').map(trimEmojis)).toEqual(lightExpected);
|
||||
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('(different behavior from emoji-mart) erases custom emoji if another is passed', () => {
|
||||
const custom = [
|
||||
{
|
||||
id: 'gabsocial',
|
||||
name: 'gabsocial',
|
||||
short_names: ['gabsocial'],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['gabsocial'],
|
||||
imageUrl: 'http://example.com',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
search('', { custom });
|
||||
emojiIndex.search('', { custom });
|
||||
const expected = [];
|
||||
expect(search('masto', { custom: [] }).map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('handles custom emoji', () => {
|
||||
const custom = [
|
||||
{
|
||||
id: 'gabsocial',
|
||||
name: 'gabsocial',
|
||||
short_names: ['gabsocial'],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: ['gabsocial'],
|
||||
imageUrl: 'http://example.com',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
search('', { custom });
|
||||
emojiIndex.search('', { custom });
|
||||
const expected = [
|
||||
{
|
||||
id: 'gabsocial',
|
||||
custom: true,
|
||||
},
|
||||
];
|
||||
expect(search('masto', { custom }).map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('masto', { custom }).map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should filter only emojis we care about, exclude pineapple', () => {
|
||||
const emojisToShowFilter = emoji => emoji.unified !== '1F34D';
|
||||
expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id))
|
||||
.not.toContain('pineapple');
|
||||
expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id))
|
||||
.not.toContain('pineapple');
|
||||
});
|
||||
|
||||
it('does an emoji whose unified name is irregular', () => {
|
||||
const expected = [
|
||||
{
|
||||
'id': 'water_polo',
|
||||
'unified': '1f93d',
|
||||
'native': '🤽',
|
||||
},
|
||||
{
|
||||
'id': 'man-playing-water-polo',
|
||||
'unified': '1f93d-200d-2642-fe0f',
|
||||
'native': '🤽♂️',
|
||||
},
|
||||
{
|
||||
'id': 'woman-playing-water-polo',
|
||||
'unified': '1f93d-200d-2640-fe0f',
|
||||
'native': '🤽♀️',
|
||||
},
|
||||
];
|
||||
expect(search('polo').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('can search for thinking_face', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'thinking_face',
|
||||
unified: '1f914',
|
||||
native: '🤔',
|
||||
},
|
||||
];
|
||||
expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('can search for woman-facepalming', () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'woman-facepalming',
|
||||
unified: '1f926-200d-2640-fe0f',
|
||||
native: '🤦♀️',
|
||||
},
|
||||
];
|
||||
expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
|
||||
expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import { autoPlayGif } from '../../initial_state';
|
||||
import unicodeMapping from './emoji_unicode_mapping_light';
|
||||
import Trie from 'substring-trie';
|
||||
|
||||
const trie = new Trie(Object.keys(unicodeMapping));
|
||||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
|
||||
const emojify = (str, customEmojis = {}) => {
|
||||
const tagCharsWithoutEmojis = '<&';
|
||||
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
|
||||
for (;;) {
|
||||
let match, i = 0, tag;
|
||||
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
|
||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||
}
|
||||
let rend, replacement = '';
|
||||
if (i === str.length) {
|
||||
break;
|
||||
} else if (str[i] === ':') {
|
||||
if (!(() => {
|
||||
rend = str.indexOf(':', i + 1) + 1;
|
||||
if (!rend) return false; // no pair of ':'
|
||||
const lt = str.indexOf('<', i + 1);
|
||||
if (!(lt === -1 || lt >= rend)) return false; // tag appeared before closing ':'
|
||||
const shortname = str.slice(i, rend);
|
||||
// now got a replacee as ':shortname:'
|
||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
||||
if (shortname in customEmojis) {
|
||||
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
|
||||
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})()) rend = ++i;
|
||||
} else if (tag >= 0) { // <, &
|
||||
rend = str.indexOf('>;'[tag], i + 1) + 1;
|
||||
if (!rend) {
|
||||
break;
|
||||
}
|
||||
if (tag === 0) {
|
||||
if (invisible) {
|
||||
if (str[i + 1] === '/') { // closing tag
|
||||
if (!--invisible) {
|
||||
tagChars = tagCharsWithEmojis;
|
||||
}
|
||||
} else if (str[rend - 2] !== '/') { // opening tag
|
||||
invisible++;
|
||||
}
|
||||
} else {
|
||||
if (str.startsWith('<span class="invisible">', i)) {
|
||||
// avoid emojifying on invisible text
|
||||
invisible = 1;
|
||||
tagChars = tagCharsWithoutEmojis;
|
||||
}
|
||||
}
|
||||
}
|
||||
i = rend;
|
||||
} else { // matched to unicode emoji
|
||||
const { filename, shortCode } = unicodeMapping[match];
|
||||
const title = shortCode ? `:${shortCode}:` : '';
|
||||
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`;
|
||||
rend = i + match.length;
|
||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
||||
if (str.codePointAt(rend) === 65038) {
|
||||
rend += 1;
|
||||
}
|
||||
}
|
||||
rtn += str.slice(0, i) + replacement;
|
||||
str = str.slice(rend);
|
||||
}
|
||||
return rtn + str;
|
||||
};
|
||||
|
||||
export default emojify;
|
||||
|
||||
export const buildCustomEmojis = (customEmojis) => {
|
||||
const emojis = [];
|
||||
|
||||
customEmojis.forEach(emoji => {
|
||||
const shortcode = emoji.get('shortcode');
|
||||
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
|
||||
const name = shortcode.replace(':', '');
|
||||
|
||||
emojis.push({
|
||||
id: name,
|
||||
name,
|
||||
short_names: [name],
|
||||
text: '',
|
||||
emoticons: [],
|
||||
keywords: [name],
|
||||
imageUrl: url,
|
||||
custom: true,
|
||||
});
|
||||
});
|
||||
|
||||
return emojis;
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
// @preval
|
||||
// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
|
||||
// This file contains the compressed version of the emoji data from
|
||||
// both emoji_map.json and from emoji-mart's emojiIndex and data objects.
|
||||
// It's designed to be emitted in an array format to take up less space
|
||||
// over the wire.
|
||||
|
||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
const emojiMap = require('./emoji_map.json');
|
||||
const { emojiIndex } = require('emoji-mart');
|
||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
||||
let data = require('emoji-mart/data/all.json');
|
||||
|
||||
if(data.compressed) {
|
||||
data = emojiMartUncompress(data);
|
||||
}
|
||||
const emojiMartData = data;
|
||||
|
||||
const excluded = ['®', '©', '™'];
|
||||
const skins = ['🏻', '🏼', '🏽', '🏾', '🏿'];
|
||||
const shortcodeMap = {};
|
||||
|
||||
const shortCodesToEmojiData = {};
|
||||
const emojisWithoutShortCodes = [];
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
shortcodeMap[emojiIndex.emojis[key].native] = emojiIndex.emojis[key].id;
|
||||
});
|
||||
|
||||
const stripModifiers = unicode => {
|
||||
skins.forEach(tone => {
|
||||
unicode = unicode.replace(tone, '');
|
||||
});
|
||||
|
||||
return unicode;
|
||||
};
|
||||
|
||||
Object.keys(emojiMap).forEach(key => {
|
||||
if (excluded.includes(key)) {
|
||||
delete emojiMap[key];
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedKey = stripModifiers(key);
|
||||
let shortcode = shortcodeMap[normalizedKey];
|
||||
|
||||
if (!shortcode) {
|
||||
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
|
||||
}
|
||||
|
||||
const filename = emojiMap[key];
|
||||
|
||||
const filenameData = [key];
|
||||
|
||||
if (unicodeToFilename(key) !== filename) {
|
||||
// filename can't be derived using unicodeToFilename
|
||||
filenameData.push(filename);
|
||||
}
|
||||
|
||||
if (typeof shortcode === 'undefined') {
|
||||
emojisWithoutShortCodes.push(filenameData);
|
||||
} else {
|
||||
if (!Array.isArray(shortCodesToEmojiData[shortcode])) {
|
||||
shortCodesToEmojiData[shortcode] = [[]];
|
||||
}
|
||||
shortCodesToEmojiData[shortcode][0].push(filenameData);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||
const { native } = emojiIndex.emojis[key];
|
||||
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||
if (short_names[0] !== key) {
|
||||
throw new Error('The compresser expects the first short_code to be the ' +
|
||||
'key. It may need to be rewritten if the emoji change such that this ' +
|
||||
'is no longer the case.');
|
||||
}
|
||||
|
||||
short_names = short_names.slice(1); // first short name can be inferred from the key
|
||||
|
||||
const searchData = [native, short_names, search];
|
||||
if (unicodeToUnifiedName(native) !== unified) {
|
||||
// unified name can't be derived from unicodeToUnifiedName
|
||||
searchData.push(unified);
|
||||
}
|
||||
|
||||
shortCodesToEmojiData[key].push(searchData);
|
||||
});
|
||||
|
||||
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
||||
// inconsistent behavior in dev mode
|
||||
module.exports = JSON.parse(JSON.stringify([
|
||||
shortCodesToEmojiData,
|
||||
emojiMartData.skins,
|
||||
emojiMartData.categories,
|
||||
emojiMartData.aliases,
|
||||
emojisWithoutShortCodes,
|
||||
]));
|
||||
File diff suppressed because one or more lines are too long
@@ -1,41 +0,0 @@
|
||||
// The output of this module is designed to mimic emoji-mart's
|
||||
// "data" object, such that we can use it for a light version of emoji-mart's
|
||||
// emojiIndex.search functionality.
|
||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
|
||||
|
||||
const emojis = {};
|
||||
|
||||
// decompress
|
||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||
let [
|
||||
filenameData, // eslint-disable-line no-unused-vars
|
||||
searchData,
|
||||
] = shortCodesToEmojiData[shortCode];
|
||||
let [
|
||||
native,
|
||||
short_names,
|
||||
search,
|
||||
unified,
|
||||
] = searchData;
|
||||
|
||||
if (!unified) {
|
||||
// unified name can be derived from unicodeToUnifiedName
|
||||
unified = unicodeToUnifiedName(native);
|
||||
}
|
||||
|
||||
short_names = [shortCode].concat(short_names);
|
||||
emojis[shortCode] = {
|
||||
native,
|
||||
search,
|
||||
short_names,
|
||||
unified,
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
emojis,
|
||||
skins,
|
||||
categories,
|
||||
short_names,
|
||||
};
|
||||
@@ -1,185 +0,0 @@
|
||||
// This code is largely borrowed from:
|
||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
||||
|
||||
import data from './emoji_mart_data_light';
|
||||
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
|
||||
|
||||
let originalPool = {};
|
||||
let index = {};
|
||||
let emojisList = {};
|
||||
let emoticonsList = {};
|
||||
let customEmojisList = [];
|
||||
|
||||
for (let emoji in data.emojis) {
|
||||
let emojiData = data.emojis[emoji];
|
||||
let { short_names, emoticons } = emojiData;
|
||||
let id = short_names[0];
|
||||
|
||||
if (emoticons) {
|
||||
emoticons.forEach(emoticon => {
|
||||
if (emoticonsList[emoticon]) {
|
||||
return;
|
||||
}
|
||||
|
||||
emoticonsList[emoticon] = id;
|
||||
});
|
||||
}
|
||||
|
||||
emojisList[id] = getSanitizedData(id);
|
||||
originalPool[id] = emojiData;
|
||||
}
|
||||
|
||||
function clearCustomEmojis(pool) {
|
||||
customEmojisList.forEach((emoji) => {
|
||||
let emojiId = emoji.id || emoji.short_names[0];
|
||||
|
||||
delete pool[emojiId];
|
||||
delete emojisList[emojiId];
|
||||
});
|
||||
}
|
||||
|
||||
function addCustomToPool(custom, pool) {
|
||||
if (customEmojisList.length) clearCustomEmojis(pool);
|
||||
|
||||
custom.forEach((emoji) => {
|
||||
let emojiId = emoji.id || emoji.short_names[0];
|
||||
|
||||
if (emojiId && !pool[emojiId]) {
|
||||
pool[emojiId] = getData(emoji);
|
||||
emojisList[emojiId] = getSanitizedData(emoji);
|
||||
}
|
||||
});
|
||||
|
||||
customEmojisList = custom;
|
||||
index = {};
|
||||
}
|
||||
|
||||
function search(value, { emojisToShowFilter, maxResults, include, exclude, custom } = {}) {
|
||||
if (custom !== undefined) {
|
||||
if (customEmojisList !== custom)
|
||||
addCustomToPool(custom, originalPool);
|
||||
} else {
|
||||
custom = [];
|
||||
}
|
||||
|
||||
maxResults = maxResults || 75;
|
||||
include = include || [];
|
||||
exclude = exclude || [];
|
||||
|
||||
let results = null,
|
||||
pool = originalPool;
|
||||
|
||||
if (value.length) {
|
||||
if (value === '-' || value === '-1') {
|
||||
return [emojisList['-1']];
|
||||
}
|
||||
|
||||
let values = value.toLowerCase().split(/[\s|,|\-|_]+/),
|
||||
allResults = [];
|
||||
|
||||
if (values.length > 2) {
|
||||
values = [values[0], values[1]];
|
||||
}
|
||||
|
||||
if (include.length || exclude.length) {
|
||||
pool = {};
|
||||
|
||||
data.categories.forEach(category => {
|
||||
let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
||||
let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
||||
if (!isIncluded || isExcluded) {
|
||||
return;
|
||||
}
|
||||
|
||||
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
|
||||
});
|
||||
|
||||
if (custom.length) {
|
||||
let customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true;
|
||||
let customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false;
|
||||
if (customIsIncluded && !customIsExcluded) {
|
||||
addCustomToPool(custom, pool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const searchValue = (value) => {
|
||||
let aPool = pool,
|
||||
aIndex = index,
|
||||
length = 0;
|
||||
|
||||
for (let charIndex = 0; charIndex < value.length; charIndex++) {
|
||||
const char = value[charIndex];
|
||||
length++;
|
||||
|
||||
aIndex[char] = aIndex[char] || {};
|
||||
aIndex = aIndex[char];
|
||||
|
||||
if (!aIndex.results) {
|
||||
let scores = {};
|
||||
|
||||
aIndex.results = [];
|
||||
aIndex.pool = {};
|
||||
|
||||
for (let id in aPool) {
|
||||
let emoji = aPool[id],
|
||||
{ search } = emoji,
|
||||
sub = value.substr(0, length),
|
||||
subIndex = search.indexOf(sub);
|
||||
|
||||
if (subIndex !== -1) {
|
||||
let score = subIndex + 1;
|
||||
if (sub === id) score = 0;
|
||||
|
||||
aIndex.results.push(emojisList[id]);
|
||||
aIndex.pool[id] = emoji;
|
||||
|
||||
scores[id] = score;
|
||||
}
|
||||
}
|
||||
|
||||
aIndex.results.sort((a, b) => {
|
||||
let aScore = scores[a.id],
|
||||
bScore = scores[b.id];
|
||||
|
||||
return aScore - bScore;
|
||||
});
|
||||
}
|
||||
|
||||
aPool = aIndex.pool;
|
||||
}
|
||||
|
||||
return aIndex.results;
|
||||
};
|
||||
|
||||
if (values.length > 1) {
|
||||
results = searchValue(value);
|
||||
} else {
|
||||
results = [];
|
||||
}
|
||||
|
||||
allResults = values.map(searchValue).filter(a => a);
|
||||
|
||||
if (allResults.length > 1) {
|
||||
allResults = intersect.apply(null, allResults);
|
||||
} else if (allResults.length) {
|
||||
allResults = allResults[0];
|
||||
}
|
||||
|
||||
results = uniq(results.concat(allResults));
|
||||
}
|
||||
|
||||
if (results) {
|
||||
if (emojisToShowFilter) {
|
||||
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
|
||||
}
|
||||
|
||||
if (results && results.length > maxResults) {
|
||||
results = results.slice(0, maxResults);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export { search };
|
||||
@@ -1,7 +0,0 @@
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
||||
|
||||
export {
|
||||
Picker,
|
||||
Emoji,
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
// A mapping of unicode strings to an object containing the filename
|
||||
// (i.e. the svg filename) and a shortCode intended to be shown
|
||||
// as a "title" attribute in an HTML element (aka tooltip).
|
||||
|
||||
const [
|
||||
shortCodesToEmojiData,
|
||||
skins, // eslint-disable-line no-unused-vars
|
||||
categories, // eslint-disable-line no-unused-vars
|
||||
short_names, // eslint-disable-line no-unused-vars
|
||||
emojisWithoutShortCodes,
|
||||
] = require('./emoji_compressed');
|
||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||
|
||||
// decompress
|
||||
const unicodeMapping = {};
|
||||
|
||||
function processEmojiMapData(emojiMapData, shortCode) {
|
||||
let [ native, filename ] = emojiMapData;
|
||||
if (!filename) {
|
||||
// filename name can be derived from unicodeToFilename
|
||||
filename = unicodeToFilename(native);
|
||||
}
|
||||
unicodeMapping[native] = {
|
||||
shortCode: shortCode,
|
||||
filename: filename,
|
||||
};
|
||||
}
|
||||
|
||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
||||
let [ filenameData ] = shortCodesToEmojiData[shortCode];
|
||||
filenameData.forEach(emojiMapData => processEmojiMapData(emojiMapData, shortCode));
|
||||
});
|
||||
emojisWithoutShortCodes.forEach(emojiMapData => processEmojiMapData(emojiMapData));
|
||||
|
||||
module.exports = unicodeMapping;
|
||||
@@ -1,258 +0,0 @@
|
||||
// This code is largely borrowed from:
|
||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
|
||||
|
||||
import data from './emoji_mart_data_light';
|
||||
|
||||
const buildSearch = (data) => {
|
||||
const search = [];
|
||||
|
||||
let addToSearch = (strings, split) => {
|
||||
if (!strings) {
|
||||
return;
|
||||
}
|
||||
|
||||
(Array.isArray(strings) ? strings : [strings]).forEach((string) => {
|
||||
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
||||
s = s.toLowerCase();
|
||||
|
||||
if (search.indexOf(s) === -1) {
|
||||
search.push(s);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addToSearch(data.short_names, true);
|
||||
addToSearch(data.name, true);
|
||||
addToSearch(data.keywords, false);
|
||||
addToSearch(data.emoticons, false);
|
||||
|
||||
return search.join(',');
|
||||
};
|
||||
|
||||
const _String = String;
|
||||
|
||||
const stringFromCodePoint = _String.fromCodePoint || function () {
|
||||
let MAX_SIZE = 0x4000;
|
||||
let codeUnits = [];
|
||||
let highSurrogate;
|
||||
let lowSurrogate;
|
||||
let index = -1;
|
||||
let length = arguments.length;
|
||||
if (!length) {
|
||||
return '';
|
||||
}
|
||||
let result = '';
|
||||
while (++index < length) {
|
||||
let codePoint = Number(arguments[index]);
|
||||
if (
|
||||
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
|
||||
codePoint < 0 || // not a valid Unicode code point
|
||||
codePoint > 0x10FFFF || // not a valid Unicode code point
|
||||
Math.floor(codePoint) !== codePoint // not an integer
|
||||
) {
|
||||
throw RangeError('Invalid code point: ' + codePoint);
|
||||
}
|
||||
if (codePoint <= 0xFFFF) { // BMP code point
|
||||
codeUnits.push(codePoint);
|
||||
} else { // Astral code point; split in surrogate halves
|
||||
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
||||
codePoint -= 0x10000;
|
||||
highSurrogate = (codePoint >> 10) + 0xD800;
|
||||
lowSurrogate = (codePoint % 0x400) + 0xDC00;
|
||||
codeUnits.push(highSurrogate, lowSurrogate);
|
||||
}
|
||||
if (index + 1 === length || codeUnits.length > MAX_SIZE) {
|
||||
result += String.fromCharCode.apply(null, codeUnits);
|
||||
codeUnits.length = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
const _JSON = JSON;
|
||||
|
||||
const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/;
|
||||
const SKINS = [
|
||||
'1F3FA', '1F3FB', '1F3FC',
|
||||
'1F3FD', '1F3FE', '1F3FF',
|
||||
];
|
||||
|
||||
function unifiedToNative(unified) {
|
||||
let unicodes = unified.split('-'),
|
||||
codePoints = unicodes.map((u) => `0x${u}`);
|
||||
|
||||
return stringFromCodePoint.apply(null, codePoints);
|
||||
}
|
||||
|
||||
function sanitize(emoji) {
|
||||
let { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji,
|
||||
id = emoji.id || short_names[0],
|
||||
colons = `:${id}:`;
|
||||
|
||||
if (custom) {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
custom,
|
||||
imageUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (skin_tone) {
|
||||
colons += `:skin-tone-${skin_tone}:`;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
colons,
|
||||
emoticons,
|
||||
unified: unified.toLowerCase(),
|
||||
skin: skin_tone || (skin_variations ? 1 : null),
|
||||
native: unifiedToNative(unified),
|
||||
};
|
||||
}
|
||||
|
||||
function getSanitizedData() {
|
||||
return sanitize(getData(...arguments));
|
||||
}
|
||||
|
||||
function getData(emoji, skin, set) {
|
||||
let emojiData = {};
|
||||
|
||||
if (typeof emoji === 'string') {
|
||||
let matches = emoji.match(COLONS_REGEX);
|
||||
|
||||
if (matches) {
|
||||
emoji = matches[1];
|
||||
|
||||
if (matches[2]) {
|
||||
skin = parseInt(matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.short_names.hasOwnProperty(emoji)) {
|
||||
emoji = data.short_names[emoji];
|
||||
}
|
||||
|
||||
if (data.emojis.hasOwnProperty(emoji)) {
|
||||
emojiData = data.emojis[emoji];
|
||||
}
|
||||
} else if (emoji.id) {
|
||||
if (data.short_names.hasOwnProperty(emoji.id)) {
|
||||
emoji.id = data.short_names[emoji.id];
|
||||
}
|
||||
|
||||
if (data.emojis.hasOwnProperty(emoji.id)) {
|
||||
emojiData = data.emojis[emoji.id];
|
||||
skin = skin || emoji.skin;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(emojiData).length) {
|
||||
emojiData = emoji;
|
||||
emojiData.custom = true;
|
||||
|
||||
if (!emojiData.search) {
|
||||
emojiData.search = buildSearch(emoji);
|
||||
}
|
||||
}
|
||||
|
||||
emojiData.emoticons = emojiData.emoticons || [];
|
||||
emojiData.variations = emojiData.variations || [];
|
||||
|
||||
if (emojiData.skin_variations && skin > 1 && set) {
|
||||
emojiData = JSON.parse(_JSON.stringify(emojiData));
|
||||
|
||||
let skinKey = SKINS[skin - 1],
|
||||
variationData = emojiData.skin_variations[skinKey];
|
||||
|
||||
if (!variationData.variations && emojiData.variations) {
|
||||
delete emojiData.variations;
|
||||
}
|
||||
|
||||
if (variationData[`has_img_${set}`]) {
|
||||
emojiData.skin_tone = skin;
|
||||
|
||||
for (let k in variationData) {
|
||||
let v = variationData[k];
|
||||
emojiData[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (emojiData.variations && emojiData.variations.length) {
|
||||
emojiData = JSON.parse(_JSON.stringify(emojiData));
|
||||
emojiData.unified = emojiData.variations.shift();
|
||||
}
|
||||
|
||||
return emojiData;
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return arr.reduce((acc, item) => {
|
||||
if (acc.indexOf(item) === -1) {
|
||||
acc.push(item);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function intersect(a, b) {
|
||||
const uniqA = uniq(a);
|
||||
const uniqB = uniq(b);
|
||||
|
||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
||||
}
|
||||
|
||||
function deepMerge(a, b) {
|
||||
let o = {};
|
||||
|
||||
for (let key in a) {
|
||||
let originalValue = a[key],
|
||||
value = originalValue;
|
||||
|
||||
if (b.hasOwnProperty(key)) {
|
||||
value = b[key];
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
value = deepMerge(originalValue, value);
|
||||
}
|
||||
|
||||
o[key] = value;
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
// https://github.com/sonicdoe/measure-scrollbar
|
||||
function measureScrollbar() {
|
||||
const div = document.createElement('div');
|
||||
|
||||
div.style.width = '100px';
|
||||
div.style.height = '100px';
|
||||
div.style.overflow = 'scroll';
|
||||
div.style.position = 'absolute';
|
||||
div.style.top = '-9999px';
|
||||
|
||||
document.body.appendChild(div);
|
||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
||||
document.body.removeChild(div);
|
||||
|
||||
return scrollbarWidth;
|
||||
}
|
||||
|
||||
export {
|
||||
getData,
|
||||
getSanitizedData,
|
||||
uniq,
|
||||
intersect,
|
||||
deepMerge,
|
||||
unifiedToNative,
|
||||
measureScrollbar,
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
// taken from:
|
||||
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
||||
exports.unicodeToFilename = (str) => {
|
||||
let result = '';
|
||||
let charCode = 0;
|
||||
let p = 0;
|
||||
let i = 0;
|
||||
while (i < str.length) {
|
||||
charCode = str.charCodeAt(i++);
|
||||
if (p) {
|
||||
if (result.length > 0) {
|
||||
result += '-';
|
||||
}
|
||||
result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
|
||||
p = 0;
|
||||
} else if (0xD800 <= charCode && charCode <= 0xDBFF) {
|
||||
p = charCode;
|
||||
} else {
|
||||
if (result.length > 0) {
|
||||
result += '-';
|
||||
}
|
||||
result += charCode.toString(16);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
function padLeft(str, num) {
|
||||
while (str.length < num) {
|
||||
str = '0' + str;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
exports.unicodeToUnifiedName = (str) => {
|
||||
let output = '';
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
if (i > 0) {
|
||||
output += '-';
|
||||
}
|
||||
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
const Explore = () => (
|
||||
<div className='column explore-page'>
|
||||
explore
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Explore;
|
||||
@@ -0,0 +1,62 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { debounce } from 'lodash';
|
||||
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
|
||||
import Column from '../../components/column';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { meUsername } from '../../initial_state';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
return {
|
||||
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Favourites extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFavouritedStatuses());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFavouritedStatuses());
|
||||
}, 300, { leading: true })
|
||||
|
||||
render () {
|
||||
const { statusIds, hasMore, isLoading, isMyAccount } = this.props;
|
||||
|
||||
if (!isMyAccount) {
|
||||
return ( <ColumnIndicator type='missing' /> );
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite gabs yet. When you favourite one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='favourited_statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,69 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { debounce } from 'lodash';
|
||||
import { meUsername } from 'gabsocial/initial_state';
|
||||
import MissingIndicator from 'gabsocial/components/missing_indicator';
|
||||
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
return {
|
||||
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
|
||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Favourites extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFavouritedStatuses());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFavouritedStatuses());
|
||||
}, 300, { leading: true })
|
||||
|
||||
render () {
|
||||
const { intl, statusIds, hasMore, isLoading, isMyAccount } = this.props;
|
||||
|
||||
if (!isMyAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite gabs yet. When you favourite one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='favourited_statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './favourited_statuses';
|
||||
@@ -1,60 +0,0 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import { fetchFavourites } from '../../actions/interactions';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Favourites extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchFavourites(nextProps.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favorited this gab yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='favourites'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
const messages = defineMessages({
|
||||
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class AccountAuthorize extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
onAuthorize: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, account, onAuthorize, onReject } = this.props;
|
||||
const content = { __html: account.get('note_emojified') };
|
||||
|
||||
return (
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Permalink href={`/${account.get('acct')}`} to={`/${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<div className='account-authorize__avatar'><Avatar account={account} size={48} /></div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
|
||||
<div className='account--panel'>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
|
||||
<div className='account--panel__button'><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from '../../../../components/permalink';
|
||||
import Avatar from '../../../../components/avatar';
|
||||
import DisplayName from '../../../../components/display_name';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../../selectors';
|
||||
import { authorizeFollowRequest, rejectFollowRequest } from '../../../../actions/accounts';
|
||||
|
||||
import './account_authorize.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getAccount(state, props.id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
onAuthorize() {
|
||||
dispatch(authorizeFollowRequest(id));
|
||||
},
|
||||
|
||||
onReject() {
|
||||
dispatch(rejectFollowRequest(id));
|
||||
},
|
||||
});
|
||||
|
||||
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class AccountAuthorize extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
onAuthorize: PropTypes.func.isRequired,
|
||||
onReject: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, account, onAuthorize, onReject } = this.props;
|
||||
const content = { __html: account.get('note_emojified') };
|
||||
|
||||
return (
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Permalink href={`/${account.get('acct')}`} to={`/${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<div className='account-authorize__avatar'>
|
||||
<Avatar account={account} size={48} />
|
||||
</div>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
|
||||
<div className='account--panel'>
|
||||
<div className='account--panel__button'>
|
||||
<IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} />
|
||||
</div>
|
||||
<div className='account--panel__button'>
|
||||
<IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.account-authorize {
|
||||
padding: 14px 10px;
|
||||
|
||||
&__avatar {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.detailed-status__display-name {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './account_authorize';
|
||||
@@ -1,25 +0,0 @@
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import AccountAuthorize from '../components/account_authorize';
|
||||
import { authorizeFollowRequest, rejectFollowRequest } from '../../../actions/accounts';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getAccount(state, props.id),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
onAuthorize () {
|
||||
dispatch(authorizeFollowRequest(id));
|
||||
},
|
||||
|
||||
onReject () {
|
||||
dispatch(rejectFollowRequest(id));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(AccountAuthorize);
|
||||
@@ -2,9 +2,9 @@ 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 LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import AccountAuthorizeContainer from './containers/account_authorize_container';
|
||||
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';
|
||||
|
||||
@@ -41,17 +41,13 @@ class FollowRequests extends ImmutablePureComponent {
|
||||
const { intl, accountIds, hasMore } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
const 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." />;
|
||||
|
||||
return (
|
||||
<Column icon='user-plus' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<Column icon='user-plus' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
@@ -59,7 +55,7 @@ class FollowRequests extends ImmutablePureComponent {
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountAuthorizeContainer key={id} id={id} />
|
||||
<AccountAuthorize key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
||||
106
app/javascript/gabsocial/features/followers/followers.js
Normal file
106
app/javascript/gabsocial/features/followers/followers.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchFollowers,
|
||||
expandFollowers,
|
||||
fetchAccountByUsername,
|
||||
} from '../../actions/accounts';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../../components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { me } from '../../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;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
}
|
||||
|
||||
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,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Followers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchFollowers(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.accountId));
|
||||
this.props.dispatch(fetchFollowers(nextProps.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandFollowers(this.props.accountId));
|
||||
}
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, accountId, unavailable } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
} else if (accountId === -1 || (!accountIds)) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
} else if (unavailable) {
|
||||
return (<ColumnIndicator type='error' message={<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,128 +1 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchFollowers,
|
||||
expandFollowers,
|
||||
fetchAccountByUsername,
|
||||
} from '../../actions/accounts';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import MissingIndicator from 'gabsocial/components/missing_indicator';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'followers', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Followers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: 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(fetchFollowers(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.accountId));
|
||||
this.props.dispatch(fetchFollowers(nextProps.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandFollowers(this.props.accountId));
|
||||
}
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, accountId, unavailable } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId == -1 || (!accountIds)) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='followers'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './followers';
|
||||
109
app/javascript/gabsocial/features/following/following.js
Normal file
109
app/javascript/gabsocial/features/following/following.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchFollowing,
|
||||
expandFollowing,
|
||||
fetchAccountByUsername,
|
||||
} from '../../actions/accounts';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../../components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
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;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
}
|
||||
|
||||
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,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Following extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(fetchFollowing(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.accountId));
|
||||
this.props.dispatch(fetchFollowing(nextProps.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandFollowing(this.props.accountId));
|
||||
}
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, accountId, unavailable } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return ( <ColumnIndicator type='missing' /> );
|
||||
} else if (accountId === -1 || (!accountIds)) {
|
||||
return ( <ColumnIndicator type='loading' /> );
|
||||
} else if (unavailable) {
|
||||
return (<ColumnIndicator type='error' message={<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='following'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,128 +1 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchFollowing,
|
||||
expandFollowing,
|
||||
fetchAccountByUsername,
|
||||
} from '../../actions/accounts';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import MissingIndicator from 'gabsocial/components/missing_indicator';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
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;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
}
|
||||
else {
|
||||
let account = accounts.find(acct => username.toLowerCase() == acct.getIn(['acct'], '').toLowerCase());
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
}
|
||||
|
||||
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,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
accountIds: state.getIn(['user_lists', 'following', accountId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Following extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: 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(fetchFollowing(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.accountId));
|
||||
this.props.dispatch(fetchFollowing(nextProps.accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandFollowing(this.props.accountId));
|
||||
}
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, isAccount, accountId, unavailable } = this.props;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId == -1 || (!accountIds)) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='following'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './following';
|
||||
@@ -1,10 +1,7 @@
|
||||
import Column from '../ui/components/column';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
|
||||
const GenericNotFound = () => (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
<ColumnIndicator type='missing' />
|
||||
);
|
||||
|
||||
export default GenericNotFound;
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnLink from '../ui/components/column_link';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { me, profile_directory } from '../../initial_state';
|
||||
import { fetchFollowRequests } from 'gabsocial/actions/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import NavigationBar from '../compose/components/navigation_bar';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import LinkFooter from 'gabsocial/features/ui/components/link_footer';
|
||||
|
||||
const messages = defineMessages({
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
|
||||
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned gabs' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
|
||||
personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
|
||||
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
||||
menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
myAccount: state.getIn(['accounts', me]),
|
||||
unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchFollowRequests: () => dispatch(fetchFollowRequests()),
|
||||
});
|
||||
|
||||
const badgeDisplay = (number, limit) => {
|
||||
if (number === 0) {
|
||||
return undefined;
|
||||
} else if (limit && number >= limit) {
|
||||
return `${limit}+`;
|
||||
} else {
|
||||
return number;
|
||||
}
|
||||
};
|
||||
|
||||
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class GettingStarted extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
myAccount: ImmutablePropTypes.map.isRequired,
|
||||
columns: ImmutablePropTypes.list,
|
||||
multiColumn: PropTypes.bool,
|
||||
fetchFollowRequests: PropTypes.func.isRequired,
|
||||
unreadFollowRequests: PropTypes.number,
|
||||
unreadNotifications: PropTypes.number,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { myAccount, fetchFollowRequests, multiColumn } = this.props;
|
||||
|
||||
if (!multiColumn && window.innerWidth >= NAVIGATION_PANEL_BREAKPOINT) {
|
||||
this.context.router.history.replace('/home');
|
||||
return;
|
||||
}
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
fetchFollowRequests();
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, myAccount, multiColumn, unreadFollowRequests } = this.props;
|
||||
|
||||
const navItems = [];
|
||||
let i = 1;
|
||||
let height = (multiColumn) ? 0 : 60;
|
||||
|
||||
if (multiColumn) {
|
||||
navItems.push(
|
||||
<ColumnSubheading key={i++} text={intl.formatMessage(messages.discover)} />,
|
||||
);
|
||||
|
||||
height += 34 + 48*2;
|
||||
|
||||
if (profile_directory) {
|
||||
navItems.push(
|
||||
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' />
|
||||
);
|
||||
|
||||
height += 48;
|
||||
}
|
||||
|
||||
navItems.push(
|
||||
<ColumnSubheading key={i++} text={intl.formatMessage(messages.personal)} />
|
||||
);
|
||||
|
||||
height += 34;
|
||||
} else if (profile_directory) {
|
||||
navItems.push(
|
||||
<ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' />
|
||||
);
|
||||
|
||||
height += 48;
|
||||
}
|
||||
|
||||
navItems.push(
|
||||
<ColumnLink key={i++} icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
|
||||
<ColumnLink key={i++} icon='star' text={intl.formatMessage(messages.favourites)} to='/favorites' />,
|
||||
<ColumnLink key={i++} icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />
|
||||
);
|
||||
|
||||
height += 48*3;
|
||||
|
||||
if (myAccount.get('locked')) {
|
||||
navItems.push(<ColumnLink key={i++} icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
|
||||
height += 48;
|
||||
}
|
||||
|
||||
if (!multiColumn) {
|
||||
navItems.push(
|
||||
<ColumnSubheading key={i++} text={intl.formatMessage(messages.settings_subheading)} />,
|
||||
<ColumnLink key={i++} icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
|
||||
);
|
||||
|
||||
height += 34 + 48;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.menu)}>
|
||||
{multiColumn && <div className='column-header__wrapper'>
|
||||
<h1 className='column-header'>
|
||||
<button>
|
||||
<Icon id='bars' className='column-header__icon' fixedWidth />
|
||||
<FormattedMessage id='getting_started.heading' defaultMessage='Getting started' />
|
||||
</button>
|
||||
</h1>
|
||||
</div>}
|
||||
|
||||
<div className='getting-started'>
|
||||
<div className='getting-started__wrapper' style={{ height }}>
|
||||
{!multiColumn && <NavigationBar account={myAccount} />}
|
||||
{navItems}
|
||||
</div>
|
||||
|
||||
{!multiColumn && <div className='flex-spacer' />}
|
||||
|
||||
<LinkFooter withHotkeys={multiColumn} />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,32 +2,32 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { changeValue, submit, setUp } from '../../../actions/group_editor';
|
||||
import Icon from '../../../components/icon';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import Column from '../../../components/column';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'groups.form.title', defaultMessage: 'Title' },
|
||||
description: { id: 'groups.form.description', defaultMessage: 'Description' },
|
||||
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload new banner image (optional)' },
|
||||
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
|
||||
update: { id: 'groups.form.update', defaultMessage: 'Update group' },
|
||||
title: { id: 'groups.form.title', defaultMessage: 'Title' },
|
||||
description: { id: 'groups.form.description', defaultMessage: 'Description' },
|
||||
coverImage: { id: 'groups.form.coverImage', defaultMessage: 'Upload new banner image (optional)' },
|
||||
coverImageChange: { id: 'groups.form.coverImageChange', defaultMessage: 'Banner image selected' },
|
||||
update: { id: 'groups.form.update', defaultMessage: 'Update group' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
title: state.getIn(['group_editor', 'title']),
|
||||
description: state.getIn(['group_editor', 'description']),
|
||||
coverImage: state.getIn(['group_editor', 'coverImage']),
|
||||
disabled: state.getIn(['group_editor', 'isSubmitting']),
|
||||
group: state.getIn(['groups', props.params.id]),
|
||||
title: state.getIn(['group_editor', 'title']),
|
||||
description: state.getIn(['group_editor', 'description']),
|
||||
coverImage: state.getIn(['group_editor', 'coverImage']),
|
||||
disabled: state.getIn(['group_editor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onTitleChange: value => dispatch(changeValue('title', value)),
|
||||
onDescriptionChange: value => dispatch(changeValue('description', value)),
|
||||
onCoverImageChange: value => dispatch(changeValue('coverImage', value)),
|
||||
onSubmit: routerHistory => dispatch(submit(routerHistory)),
|
||||
setUp: group => dispatch(setUp(group)),
|
||||
onTitleChange: value => dispatch(changeValue('title', value)),
|
||||
onDescriptionChange: value => dispatch(changeValue('description', value)),
|
||||
onCoverImageChange: value => dispatch(changeValue('coverImage', value)),
|
||||
onSubmit: routerHistory => dispatch(submit(routerHistory)),
|
||||
setUp: group => dispatch(setUp(group)),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@@ -35,111 +35,103 @@ export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
class Edit extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
group: ImmutablePropTypes.map,
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
coverImage: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
group: ImmutablePropTypes.map,
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
coverImage: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount(nextProps) {
|
||||
if (this.props.group) {
|
||||
this.props.setUp(this.props.group);
|
||||
}
|
||||
componentWillMount(nextProps) {
|
||||
if (this.props.group) {
|
||||
this.props.setUp(this.props.group);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.group && nextProps.group) {
|
||||
this.props.setUp(nextProps.group);
|
||||
}
|
||||
if (!this.props.group && nextProps.group) {
|
||||
this.props.setUp(nextProps.group);
|
||||
}
|
||||
}
|
||||
|
||||
handleTitleChange = e => {
|
||||
this.props.onTitleChange(e.target.value);
|
||||
this.props.onTitleChange(e.target.value);
|
||||
}
|
||||
|
||||
handleDescriptionChange = e => {
|
||||
this.props.onDescriptionChange(e.target.value);
|
||||
this.props.onDescriptionChange(e.target.value);
|
||||
}
|
||||
|
||||
handleCoverImageChange = e => {
|
||||
this.props.onCoverImageChange(e.target.files[0]);
|
||||
this.props.onCoverImageChange(e.target.files[0]);
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit(this.context.router.history);
|
||||
e.preventDefault();
|
||||
this.props.onSubmit(this.context.router.history);
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit(this.context.router.history);
|
||||
this.props.onSubmit(this.context.router.history);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { group, title, description, coverImage, disabled, intl } = this.props;
|
||||
|
||||
if (typeof group === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
} else if (group === false) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
const { group, title, description, coverImage, disabled, intl } = this.props;
|
||||
|
||||
return (
|
||||
<form className='group-form' onSubmit={this.handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
className='standard'
|
||||
type='text'
|
||||
value={title}
|
||||
disabled={disabled}
|
||||
onChange={this.handleTitleChange}
|
||||
placeholder={intl.formatMessage(messages.title)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea
|
||||
className='standard'
|
||||
type='text'
|
||||
value={description}
|
||||
disabled={disabled}
|
||||
onChange={this.handleDescriptionChange}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
/>
|
||||
</div>
|
||||
if (typeof group === 'undefined') {
|
||||
return ( <ColumnIndicator type='loading' /> );
|
||||
} else if (group === false) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
}
|
||||
|
||||
<div>
|
||||
<label htmlFor='group_cover_image' className={classNames('group-form__file-label', { 'group-form__file-label--selected': coverImage !== null })}>
|
||||
{intl.formatMessage(coverImage === null ? messages.coverImage : messages.coverImageChange)}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type='file'
|
||||
className='group-form__file'
|
||||
id='group_cover_image'
|
||||
disabled={disabled}
|
||||
onChange={this.handleCoverImageChange}
|
||||
/>
|
||||
return (
|
||||
<form className='group-form' onSubmit={this.handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
className='standard'
|
||||
type='text'
|
||||
value={title}
|
||||
disabled={disabled}
|
||||
onChange={this.handleTitleChange}
|
||||
placeholder={intl.formatMessage(messages.title)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button>{intl.formatMessage(messages.update)}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
<div>
|
||||
<textarea
|
||||
className='standard'
|
||||
type='text'
|
||||
value={description}
|
||||
disabled={disabled}
|
||||
onChange={this.handleDescriptionChange}
|
||||
placeholder={intl.formatMessage(messages.description)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor='group_cover_image' className={classNames('group-form__file-label', { 'group-form__file-label--selected': coverImage !== null })}>
|
||||
{intl.formatMessage(coverImage === null ? messages.coverImage : messages.coverImageChange)}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type='file'
|
||||
className='group-form__file'
|
||||
id='group_cover_image'
|
||||
disabled={disabled}
|
||||
onChange={this.handleCoverImageChange}
|
||||
/>
|
||||
|
||||
<button>{intl.formatMessage(messages.update)}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,70 +1,67 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import {
|
||||
fetchMembers,
|
||||
expandMembers,
|
||||
fetchMembers,
|
||||
expandMembers,
|
||||
} from '../../../actions/groups';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Column from '../../ui/components/column';
|
||||
import Column from '../../../components/column';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups', id, 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class GroupMembers extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { id } } = this.props;
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchMembers(id));
|
||||
this.props.dispatch(fetchMembers(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchMembers(nextProps.params.id));
|
||||
}
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchMembers(nextProps.params.id));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandMembers(this.props.params.id));
|
||||
this.props.dispatch(expandMembers(this.props.params.id));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, group } = this.props;
|
||||
const { accountIds, hasMore, group } = this.props;
|
||||
|
||||
if (!group || !accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
if (!group || !accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='members'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />}
|
||||
>
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='members'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='group.members.empty' defaultMessage='This group does not has any members.' />}
|
||||
>
|
||||
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { debounce } from 'lodash';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
import ColumnIndicator from '../../../components/column_indicator';
|
||||
import {
|
||||
fetchRemovedAccounts,
|
||||
expandRemovedAccounts,
|
||||
removeRemovedAccount,
|
||||
fetchRemovedAccounts,
|
||||
expandRemovedAccounts,
|
||||
removeRemovedAccount,
|
||||
} from '../../../actions/groups';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Column from '../../ui/components/column';
|
||||
import Column from '../../../components/column';
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'groups.removed_accounts', defaultMessage: 'Allow joining' },
|
||||
remove: { id: 'groups.removed_accounts', defaultMessage: 'Allow joining' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params: { id } }) => ({
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups_removed_accounts', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups_removed_accounts', id, 'next']),
|
||||
group: state.getIn(['groups', id]),
|
||||
accountIds: state.getIn(['user_lists', 'groups_removed_accounts', id, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'groups_removed_accounts', id, 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@@ -28,56 +28,53 @@ export default @connect(mapStateToProps)
|
||||
class GroupRemovedAccounts extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { params: { id } } = this.props;
|
||||
const { params: { id } } = this.props;
|
||||
|
||||
this.props.dispatch(fetchRemovedAccounts(id));
|
||||
this.props.dispatch(fetchRemovedAccounts(id));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchRemovedAccounts(nextProps.params.id));
|
||||
}
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.props.dispatch(fetchRemovedAccounts(nextProps.params.id));
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandRemovedAccounts(this.props.params.id));
|
||||
this.props.dispatch(expandRemovedAccounts(this.props.params.id));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { accountIds, hasMore, group, intl } = this.props;
|
||||
const { accountIds, hasMore, group, intl } = this.props;
|
||||
|
||||
if (!group || !accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
if (!group || !accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='removed_accounts'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='group.removed_accounts.empty' defaultMessage='This group does not has any removed accounts.' />}
|
||||
>
|
||||
{accountIds.map(id => <AccountContainer
|
||||
key={id}
|
||||
id={id}
|
||||
actionIcon="remove"
|
||||
onActionClick={() => this.props.dispatch(removeRemovedAccount(group.get('id'), id))}
|
||||
actionTitle={intl.formatMessage(messages.remove)}
|
||||
/>)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='removed_accounts'
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='group.removed_accounts.empty' defaultMessage='This group does not has any removed accounts.' />}
|
||||
>
|
||||
{accountIds.map(id => (<AccountContainer
|
||||
key={id}
|
||||
id={id}
|
||||
actionIcon='remove'
|
||||
onActionClick={() => this.props.dispatch(removeRemovedAccount(group.get('id'), id))}
|
||||
actionTitle={intl.formatMessage(messages.remove)}
|
||||
/>))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,102 +1,94 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import StatusListContainer from '../../ui/containers/status_list_container';
|
||||
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 MissingIndicator from '../../../components/missing_indicator';
|
||||
import LoadingIndicator from '../../../components/loading_indicator';
|
||||
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';
|
||||
|
||||
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,
|
||||
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,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class GroupTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
relationships: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
group: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
relationships: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(expandGroupTimeline(id));
|
||||
dispatch(expandGroupTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectGroupStream(id));
|
||||
this.disconnect = dispatch(connectGroupStream(id));
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandGroupTimeline(id, { maxId }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { columnId, group, relationships, account } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const { columnId, group, relationships, account } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
if (typeof group === 'undefined' || !relationships) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
} else if (group === false) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
if (typeof group === 'undefined' || !relationships) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
} else if (group === false) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
}
|
||||
|
||||
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={true} autoFocus={false}/>
|
||||
</div>
|
||||
)}
|
||||
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 className='group__feed'>
|
||||
<StatusListContainer
|
||||
alwaysPrepend
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
timelineId={`group:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
group={group}
|
||||
withGroupAdmin={relationships && relationships.get('admin')}
|
||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<div className='group__feed'>
|
||||
<StatusListContainer
|
||||
scrollKey={`group_timeline-${columnId}`}
|
||||
timelineId={`group:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
group={group}
|
||||
withGroupAdmin={relationships && relationships.get('admin')}
|
||||
emptyMessage={<FormattedMessage id='empty_column.group' defaultMessage='There is nothing in this group yet. When members of this group post new statuses, they will appear here.' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import StatusListContainer from '../../containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connectHashtagStream } from '../../actions/streaming';
|
||||
import { isEqual } from 'lodash';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class HashtagTimeline extends PureComponent {
|
||||
|
||||
disconnects = [];
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
};
|
||||
|
||||
title = () => {
|
||||
let title = [this.props.params.id];
|
||||
|
||||
if (this.additionalFor('any')) {
|
||||
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
|
||||
}
|
||||
|
||||
if (this.additionalFor('all')) {
|
||||
title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
|
||||
}
|
||||
|
||||
if (this.additionalFor('none')) {
|
||||
title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
additionalFor = (mode) => {
|
||||
const { tags } = this.props.params;
|
||||
|
||||
if (tags && (tags[mode] || []).length > 0) {
|
||||
return tags[mode].map(tag => tag.value).join('/');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
_subscribe (dispatch, id, tags = {}) {
|
||||
let any = (tags.any || []).map(tag => tag.value);
|
||||
let all = (tags.all || []).map(tag => tag.value);
|
||||
let none = (tags.none || []).map(tag => tag.value);
|
||||
|
||||
[id, ...any].map(tag => {
|
||||
this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
|
||||
let tags = status.tags.map(tag => tag.name);
|
||||
|
||||
return all.filter(tag => tags.includes(tag)).length === all.length &&
|
||||
none.filter(tag => tags.includes(tag)).length === 0;
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
_unsubscribe () {
|
||||
this.disconnects.map(disconnect => disconnect());
|
||||
this.disconnects = [];
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id, tags } = this.props.params;
|
||||
|
||||
this._subscribe(dispatch, id, tags);
|
||||
dispatch(expandHashtagTimeline(id, { tags }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch, params } = this.props;
|
||||
const { id, tags } = nextProps.params;
|
||||
|
||||
if (id !== params.id || !isEqual(tags, params.tags)) {
|
||||
this._unsubscribe();
|
||||
this._subscribe(dispatch, id, tags);
|
||||
this.props.dispatch(clearTimeline(`hashtag:${id}`));
|
||||
this.props.dispatch(expandHashtagTimeline(id, { tags }));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id, tags } = this.props.params;
|
||||
this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hasUnread } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
return (
|
||||
<Column heading={`#${id}`}>
|
||||
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()} />
|
||||
<StatusListContainer
|
||||
scrollKey='hashtag_timeline'
|
||||
timelineId={`hashtag:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,120 +1 @@
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { expandHashtagTimeline, clearTimeline } from '../../actions/timelines';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connectHashtagStream } from '../../actions/streaming';
|
||||
import { isEqual } from 'lodash';
|
||||
import ColumnBackButton from '../../components/column_back_button';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class HashtagTimeline extends PureComponent {
|
||||
|
||||
disconnects = [];
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
};
|
||||
|
||||
title = () => {
|
||||
let title = [this.props.params.id];
|
||||
|
||||
if (this.additionalFor('any')) {
|
||||
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
|
||||
}
|
||||
|
||||
if (this.additionalFor('all')) {
|
||||
title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
|
||||
}
|
||||
|
||||
if (this.additionalFor('none')) {
|
||||
title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
additionalFor = (mode) => {
|
||||
const { tags } = this.props.params;
|
||||
|
||||
if (tags && (tags[mode] || []).length > 0) {
|
||||
return tags[mode].map(tag => tag.value).join('/');
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
_subscribe (dispatch, id, tags = {}) {
|
||||
let any = (tags.any || []).map(tag => tag.value);
|
||||
let all = (tags.all || []).map(tag => tag.value);
|
||||
let none = (tags.none || []).map(tag => tag.value);
|
||||
|
||||
[id, ...any].map(tag => {
|
||||
this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
|
||||
let tags = status.tags.map(tag => tag.name);
|
||||
|
||||
return all.filter(tag => tags.includes(tag)).length === all.length &&
|
||||
none.filter(tag => tags.includes(tag)).length === 0;
|
||||
})));
|
||||
});
|
||||
}
|
||||
|
||||
_unsubscribe () {
|
||||
this.disconnects.map(disconnect => disconnect());
|
||||
this.disconnects = [];
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id, tags } = this.props.params;
|
||||
|
||||
this._subscribe(dispatch, id, tags);
|
||||
dispatch(expandHashtagTimeline(id, { tags }));
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const { dispatch, params } = this.props;
|
||||
const { id, tags } = nextProps.params;
|
||||
|
||||
if (id !== params.id || !isEqual(tags, params.tags)) {
|
||||
this._unsubscribe();
|
||||
this._subscribe(dispatch, id, tags);
|
||||
this.props.dispatch(clearTimeline(`hashtag:${id}`));
|
||||
this.props.dispatch(expandHashtagTimeline(id, { tags }));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id, tags } = this.props.params;
|
||||
this.props.dispatch(expandHashtagTimeline(id, { maxId, tags }));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hasUnread } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
return (
|
||||
<Column label={`#${id}`}>
|
||||
<ColumnBackButton />
|
||||
<ColumnHeader icon='hashtag' active={hasUnread} title={this.title()} />
|
||||
<StatusListContainer
|
||||
scrollKey='hashtag_timeline'
|
||||
timelineId={`hashtag:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './hashtag_timeline';
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
import SettingToggle from '../../../components/setting_toggle';
|
||||
import { changeSetting, saveSettings } from '../../../actions/settings';
|
||||
|
||||
export default @injectIntl
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'home']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange(key, checked) {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
|
||||
onSave() {
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ColumnSettings extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -19,11 +37,23 @@ class ColumnSettings extends PureComponent {
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />} />
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reblog']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reply']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
import { changeSetting, saveSettings } from '../../../actions/settings';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
settings: state.getIn(['settings', 'home']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onChange (key, checked) {
|
||||
dispatch(changeSetting(['home', ...key], checked));
|
||||
},
|
||||
|
||||
onSave () {
|
||||
dispatch(saveSettings());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
||||
@@ -1,9 +1,9 @@
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import HomeColumnHeader from '../../components/home_column_header';
|
||||
import { expandHomeTimeline } from '../../actions/timelines';
|
||||
import StatusListContainer from '../../containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import ColumnSettings from './components/column_settings';
|
||||
import HomeColumnHeader from '../../components/column_header';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.home', defaultMessage: 'Home' },
|
||||
@@ -66,10 +66,10 @@ class HomeTimeline extends PureComponent {
|
||||
const { intl, hasUnread } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<HomeColumnHeader activeItem='home' active={hasUnread}>
|
||||
<ColumnSettingsContainer />
|
||||
</HomeColumnHeader>
|
||||
<Column heading={intl.formatMessage(messages.title)}>
|
||||
{ /* <HomeColumnHeader activeItem='home' active={hasUnread}>
|
||||
<ColumnSettings />
|
||||
</HomeColumnHeader> */}
|
||||
<StatusListContainer
|
||||
scrollKey='home_timeline'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
|
||||
@@ -1,167 +1 @@
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { closeOnboarding } from '../../actions/onboarding';
|
||||
|
||||
const FrameWelcome = ({ domain, onNext }) => (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--centered'>
|
||||
<h3><FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' /></h3>
|
||||
<p><FormattedMessage id='introduction.welcome.text' defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name." values={{ domain: <code>{domain}</code> }} /></p>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
FrameWelcome.propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const FrameFederation = ({ onNext }) => (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--columnized'>
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' /></h3>
|
||||
<p><FormattedMessage id='introduction.federation.home.text' defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!' /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.federation.action' defaultMessage='Next' /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
FrameFederation.propTypes = {
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const FrameInteractions = ({ onNext }) => (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--columnized'>
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' /></h3>
|
||||
<p><FormattedMessage id='introduction.interactions.reply.text' defaultMessage="You can reply to other people's and your own gabs, which will chain them together in a conversation." /></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Repost' /></h3>
|
||||
<p><FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's gabs with your followers by reposting them." /></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3><FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favorite' /></h3>
|
||||
<p><FormattedMessage id='introduction.interactions.favourite.text' defaultMessage='You can save a gab for later, and let the author know that you liked it, by favouriting it.' /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}><FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' /></button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
FrameInteractions.propTypes = {
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default @connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
|
||||
class Introduction extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
currentIndex: 0,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.pages = [
|
||||
<FrameWelcome domain={this.props.domain} onNext={this.handleNext} />,
|
||||
<FrameFederation onNext={this.handleNext} />,
|
||||
<FrameInteractions onNext={this.handleFinish} />,
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
handleDot = (e) => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
this.setState({ currentIndex: i });
|
||||
}
|
||||
|
||||
handlePrev = () => {
|
||||
this.setState(({ currentIndex }) => ({
|
||||
currentIndex: Math.max(0, currentIndex - 1),
|
||||
}));
|
||||
}
|
||||
|
||||
handleNext = () => {
|
||||
const { pages } = this;
|
||||
|
||||
this.setState(({ currentIndex }) => ({
|
||||
currentIndex: Math.min(currentIndex + 1, pages.length - 1),
|
||||
}));
|
||||
}
|
||||
|
||||
handleSwipe = (index) => {
|
||||
this.setState({ currentIndex: index });
|
||||
}
|
||||
|
||||
handleFinish = () => {
|
||||
this.props.dispatch(closeOnboarding());
|
||||
}
|
||||
|
||||
handleKeyUp = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
this.handlePrev();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.handleNext();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { currentIndex } = this.state;
|
||||
const { pages } = this;
|
||||
|
||||
return (
|
||||
<div className='introduction'>
|
||||
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='introduction__pager'>
|
||||
{pages.map((page, i) => (
|
||||
<div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
|
||||
))}
|
||||
</ReactSwipeableViews>
|
||||
|
||||
<div className='introduction__dots'>
|
||||
{pages.map((_, i) => (
|
||||
<div
|
||||
key={`dot-${i}`}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
data-index={i}
|
||||
onClick={this.handleDot}
|
||||
className={classNames('introduction__dot', { active: i === currentIndex })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './introduction';
|
||||
|
||||
237
app/javascript/gabsocial/features/introduction/introduction.js
Normal file
237
app/javascript/gabsocial/features/introduction/introduction.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { closeOnboarding } from '../../actions/onboarding';
|
||||
|
||||
import './introduction.scss';
|
||||
|
||||
class FrameWelcome extends Component {
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.domain !== this.props.domain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { domain, onNext } = this.props;
|
||||
|
||||
return (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--centered'>
|
||||
<h3>
|
||||
<FormattedMessage id='introduction.welcome.headline' defaultMessage='First steps' />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='introduction.welcome.text'
|
||||
defaultMessage="Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name."
|
||||
values={{
|
||||
domain: <code>{domain}</code>
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}>
|
||||
<FormattedMessage id='introduction.welcome.action' defaultMessage="Let's go!" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FrameFederation extends Component {
|
||||
static propTypes = {
|
||||
onNext: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--columnized'>
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage id='introduction.federation.home.headline' defaultMessage='Home' />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='introduction.federation.home.text'
|
||||
defaultMessage='Posts from people you follow will appear in your home feed. You can follow anyone on any server!'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}>
|
||||
<FormattedMessage id='introduction.federation.action' defaultMessage='Next' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
class FrameInteractions extends Component {
|
||||
static propTypes = {
|
||||
onNext: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='introduction__frame'>
|
||||
<div className='introduction__text introduction__text--columnized'>
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage id='introduction.interactions.reply.headline' defaultMessage='Reply' />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='introduction.interactions.reply.text'
|
||||
defaultMessage="You can reply to other people's and your own gabs, which will chain them together in a conversation."
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage id='introduction.interactions.reblog.headline' defaultMessage='Repost' />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage id='introduction.interactions.reblog.text' defaultMessage="You can share other people's gabs with your followers by reposting them." />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage id='introduction.interactions.favourite.headline' defaultMessage='Favorite' />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage id='introduction.interactions.favourite.text' defaultMessage='You can save a gab for later, and let the author know that you liked it, by favouriting it.' />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='introduction__action'>
|
||||
<button className='button' onClick={onNext}>
|
||||
<FormattedMessage id='introduction.interactions.action' defaultMessage='Finish tutorial!' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default @connect(state => ({ domain: state.getIn(['meta', 'domain']) }))
|
||||
class Introduction extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
domain: PropTypes.string.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
currentIndex: 0,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.pages = [
|
||||
<FrameWelcome domain={this.props.domain} onNext={this.handleNext} />,
|
||||
<FrameFederation onNext={this.handleNext} />,
|
||||
<FrameInteractions onNext={this.handleFinish} />,
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.addEventListener('keyup', this.handleKeyUp);
|
||||
}
|
||||
|
||||
handleDot = (e) => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
this.setState({ currentIndex: i });
|
||||
}
|
||||
|
||||
handlePrev = () => {
|
||||
this.setState(({ currentIndex }) => ({
|
||||
currentIndex: Math.max(0, currentIndex - 1),
|
||||
}));
|
||||
}
|
||||
|
||||
handleNext = () => {
|
||||
const { pages } = this;
|
||||
|
||||
this.setState(({ currentIndex }) => ({
|
||||
currentIndex: Math.min(currentIndex + 1, pages.length - 1),
|
||||
}));
|
||||
}
|
||||
|
||||
handleSwipe = (index) => {
|
||||
this.setState({ currentIndex: index });
|
||||
}
|
||||
|
||||
handleFinish = () => {
|
||||
this.props.dispatch(closeOnboarding());
|
||||
}
|
||||
|
||||
handleKeyUp = ({ key }) => {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
this.handlePrev();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
this.handleNext();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { currentIndex } = this.state;
|
||||
const { pages } = this;
|
||||
|
||||
return (
|
||||
<div className='introduction'>
|
||||
<ReactSwipeableViews index={currentIndex} onChangeIndex={this.handleSwipe} className='introduction__pager'>
|
||||
{pages.map((page, i) => (
|
||||
<div key={i} className={classNames('introduction__frame-wrapper', { 'active': i === currentIndex })}>{page}</div>
|
||||
))}
|
||||
</ReactSwipeableViews>
|
||||
|
||||
<div className='introduction__dots'>
|
||||
{pages.map((_, i) => (
|
||||
<div
|
||||
key={`dot-${i}`}
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
data-index={i}
|
||||
onClick={this.handleDot}
|
||||
className={classNames('introduction__dot', { active: i === currentIndex })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
143
app/javascript/gabsocial/features/introduction/introduction.scss
Normal file
143
app/javascript/gabsocial/features/introduction/introduction.scss
Normal file
@@ -0,0 +1,143 @@
|
||||
.introduction {
|
||||
@include flex(center, center, column);
|
||||
|
||||
@media screen and (max-width: 920px) {
|
||||
background: darken($ui-base-color, 8%);
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
&__pager {
|
||||
background: darken($ui-base-color, 8%);
|
||||
box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__pager,
|
||||
&__frame {
|
||||
border-radius: 10px;
|
||||
width: 50vw;
|
||||
min-width: 920px;
|
||||
|
||||
@media screen and (max-width: 920px) {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__frame-wrapper {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms linear;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
transition: opacity 50ms linear;
|
||||
}
|
||||
}
|
||||
|
||||
&__frame {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__illustration {
|
||||
height: 50vh;
|
||||
|
||||
@media screen and (max-width: 630px) {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
margin: 0;
|
||||
|
||||
@include size(100%);
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
&--columnized {
|
||||
display: flex;
|
||||
|
||||
&>div {
|
||||
flex: 1 1 33.33%;
|
||||
text-align: center;
|
||||
padding: 25px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 630px) {
|
||||
display: block;
|
||||
padding: 15px 0;
|
||||
padding-bottom: 20px;
|
||||
|
||||
&>div {
|
||||
padding: 10px 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@include text-sizing(24px, 700, 1.5);
|
||||
}
|
||||
|
||||
p {
|
||||
color: $darker-text-color;
|
||||
|
||||
@include text-sizing(16px, 400, 24px);
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background: darken($ui-base-color, 8%);
|
||||
font-size: 15px;
|
||||
padding: 1px 3px;
|
||||
|
||||
@include border-design(lighten($ui-base-color, 8%), 1px, 2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--centered {
|
||||
padding: 25px;
|
||||
padding-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__dots {
|
||||
padding: 25px;
|
||||
|
||||
@include flex(center, center);
|
||||
|
||||
@media screen and (max-width: 630px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__dot {
|
||||
background: transparent;
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
|
||||
@include size(14px);
|
||||
@include border-design($ui-highlight-color, 1px, 14px);
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
&.active {
|
||||
cursor: default;
|
||||
background: $ui-highlight-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
padding: 25px;
|
||||
padding-top: 0;
|
||||
|
||||
@include flex(center, center);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import List from './components/list';
|
||||
import Account from './components/account';
|
||||
import IconButton from 'gabsocial/components/icon_button';
|
||||
import IconButton from '../../components/icon_button';
|
||||
import NewListForm from '../lists/components/new_list_form';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import ColumnSubheading from '../../components/column_subheading/column_subheading';
|
||||
// hack
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Button from 'gabsocial/components/button';
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
import Icon from '../../../components/icon';
|
||||
import Button from '../../../components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
||||
|
||||
@@ -5,8 +5,8 @@ import { setupListEditor, clearListSuggestions, resetListEditor } from '../../ac
|
||||
import Account from './components/account';
|
||||
import Search from './components/search';
|
||||
import EditListForm from './components/edit_list_form';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import IconButton from 'gabsocial/components/icon_button';
|
||||
import ColumnSubheading from '../../components/column_subheading';
|
||||
import IconButton from '../../components/icon_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
|
||||
@@ -1,156 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import StatusListContainer from '../ui/containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { connectListStream } from '../../actions/streaming';
|
||||
import { expandListTimeline } from '../../actions/timelines';
|
||||
import { fetchList, deleteList } from '../../actions/lists';
|
||||
import { openModal } from '../../actions/modal';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import HomeColumnHeader from '../../components/home_column_header';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Button from 'gabsocial/components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
list: state.getIn(['lists', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ListTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this.handleConnect(this.props.params.id);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.handleDisconnect();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.handleDisconnect();
|
||||
this.handleConnect(nextProps.params.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleConnect(id) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(fetchList(id));
|
||||
dispatch(expandListTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectListStream(id));
|
||||
}
|
||||
|
||||
handleDisconnect() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteList(id));
|
||||
this.context.router.history.push('/lists');
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hasUnread, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const title = list ? list.get('title') : id;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<div>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
} else if (list === false) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = (
|
||||
<div>
|
||||
<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />
|
||||
<br/><br/>
|
||||
<Button onClick={this.handleEditClick}><FormattedMessage id='list.click_to_add' defaultMessage='Click here to add people'/></Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column label={title}>
|
||||
<HomeColumnHeader activeItem='lists' activeSubItem={id} active={hasUnread}>
|
||||
<div className='column-header__links'>
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
|
||||
<hr/>
|
||||
|
||||
<Link to='/lists' className='text-btn column-header__setting-btn column-header__setting-btn--link'>
|
||||
<FormattedMessage id='lists.view_all' defaultMessage='View all lists' />
|
||||
<Icon id='arrow-right' />
|
||||
</Link>
|
||||
</div>
|
||||
</HomeColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
scrollKey='list_timeline'
|
||||
timelineId={`list:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './list_timeline';
|
||||
|
||||
146
app/javascript/gabsocial/features/list_timeline/list_timeline.js
Normal file
146
app/javascript/gabsocial/features/list_timeline/list_timeline.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import StatusListContainer from '../../containers/status_list_container';
|
||||
import Column from '../../components/column';
|
||||
import { connectListStream } from '../../actions/streaming';
|
||||
import { expandListTimeline } from '../../actions/timelines';
|
||||
import { fetchList, deleteList } from '../../actions/lists';
|
||||
import { openModal } from '../../actions/modal';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Icon from '../../components/icon';
|
||||
import HomeColumnHeader from '../../components/column_header';
|
||||
import Button from '../../components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
list: state.getIn(['lists', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ListTimeline extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.handleConnect(this.props.params.id);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.handleDisconnect();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.handleDisconnect();
|
||||
this.handleConnect(nextProps.params.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleConnect(id) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(fetchList(id));
|
||||
dispatch(expandListTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectListStream(id));
|
||||
}
|
||||
|
||||
handleDisconnect() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||
}
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
|
||||
}
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteList(id));
|
||||
this.context.router.history.push('/lists');
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasUnread, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const title = list ? list.get('title') : id;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
} else if (list === false) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
}
|
||||
|
||||
const emptyMessage = (
|
||||
<div>
|
||||
<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />
|
||||
<br /><br />
|
||||
<Button onClick={this.handleEditClick}><FormattedMessage id='list.click_to_add' defaultMessage='Click here to add people' /></Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column heading={title}>
|
||||
<HomeColumnHeader activeItem='lists' activeSubItem={id} active={hasUnread}>
|
||||
<div className='column-header__links'>
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<Link to='/lists' className='text-btn column-header__setting-btn column-header__setting-btn--link'>
|
||||
<FormattedMessage id='lists.view_all' defaultMessage='View all lists' />
|
||||
<Icon id='arrow-right' />
|
||||
</Link>
|
||||
</div>
|
||||
</HomeColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
scrollKey='list_timeline'
|
||||
timelineId={`list:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import { fetchLists } from '../../actions/lists';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ColumnLink from '../ui/components/column_link';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
import ColumnLink from '../../components/column_link';
|
||||
import ColumnSubheading from '../../components/column_subheading';
|
||||
import NewListForm from './components/new_list_form';
|
||||
import { createSelector } from 'reselect';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
@@ -47,21 +47,17 @@ class Lists extends ImmutablePureComponent {
|
||||
const { intl, lists } = this.props;
|
||||
|
||||
if (!lists) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column icon='list-ul' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<br/>
|
||||
<Column icon='list-ul' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.add)} />
|
||||
<NewListForm />
|
||||
<br/>
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
|
||||
@@ -1,69 +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 LoadingIndicator from '../../components/loading_indicator';
|
||||
import Column from '../ui/components/column';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Mutes 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(fetchMutes());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandMutes());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, hasMore, accountIds } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
|
||||
|
||||
return (
|
||||
<Column icon='volume-off' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<ScrollableList
|
||||
scrollKey='mutes'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './mutes';
|
||||
65
app/javascript/gabsocial/features/mutes/mutes.js
Normal file
65
app/javascript/gabsocial/features/mutes/mutes.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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 AccountContainer from '../../containers/account_container';
|
||||
import { fetchMutes, expandMutes } from '../../actions/mutes';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'mutes', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Mutes 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(fetchMutes());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandMutes());
|
||||
}, 300, { leading: true });
|
||||
|
||||
render() {
|
||||
const { intl, hasMore, accountIds } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
|
||||
|
||||
return (
|
||||
<Column icon='volume-off' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='mutes'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
export default class ClearColumnButton extends PureComponent {
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ClearColumnButton from './clear_column_button';
|
||||
import SettingToggle from './setting_toggle';
|
||||
import SettingToggle from '../../../components/setting_toggle';
|
||||
|
||||
export default class ColumnSettings extends PureComponent {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
const tooltips = defineMessages({
|
||||
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import Icon from '../../../components/icon';
|
||||
|
||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||
const output = [message];
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
export default class SettingToggle extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
prefix: PropTypes.string,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
settingPath: PropTypes.array.isRequired,
|
||||
label: PropTypes.node.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onChange = ({ target }) => {
|
||||
this.props.onChange(this.props.settingPath, target.checked);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { prefix, settings, settingPath, label } = this.props;
|
||||
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
|
||||
|
||||
return (
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
|
||||
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { createSelector } from 'reselect';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import LoadGap from '../../components/load_gap';
|
||||
import LoadMore from '../../components/load_more';
|
||||
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -136,7 +136,8 @@ class Notifications extends PureComponent {
|
||||
scrollableContent = this.scrollableContent;
|
||||
} else if (notifications.size > 0 || hasMore) {
|
||||
scrollableContent = notifications.map((item, index) => item === null ? (
|
||||
<LoadGap
|
||||
<LoadMore
|
||||
gap
|
||||
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||
@@ -173,7 +174,7 @@ class Notifications extends PureComponent {
|
||||
);
|
||||
|
||||
return (
|
||||
<Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
|
||||
<Column ref={this.setColumnRef} heading={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader icon='bell' active={isUnread} title={intl.formatMessage(messages.title)}>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
||||
@@ -1,57 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
||||
import Column from '../ui/components/column';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { meUsername } from 'gabsocial/initial_state';
|
||||
import MissingIndicator from 'gabsocial/components/missing_indicator';
|
||||
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
return {
|
||||
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
|
||||
statusIds: state.getIn(['status_lists', 'pins', 'items']),
|
||||
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
||||
};
|
||||
};;
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class PinnedStatuses extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchPinnedStatuses());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, statusIds, hasMore, isMyAccount } = this.props;
|
||||
|
||||
if (!isMyAccount) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='pinned_statuses'
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='pinned_statuses.none' defaultMessage='No pins to show.'/>}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './pinned_statuses';
|
||||
@@ -0,0 +1,51 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
||||
import Column from '../../components/column';
|
||||
import StatusList from '../../components/status_list/status_list';
|
||||
import { meUsername } from '../../initial_state';
|
||||
import ColumnIndicator from '../../components/column_indicator/column_indicator';
|
||||
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
return {
|
||||
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
|
||||
statusIds: state.getIn(['status_lists', 'pins', 'items']),
|
||||
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
|
||||
};
|
||||
};;
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class PinnedStatuses extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchPinnedStatuses());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { statusIds, hasMore, isMyAccount } = this.props;
|
||||
|
||||
if (!isMyAccount) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='pinned_statuses'
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='pinned_statuses.none' defaultMessage='No pins to show.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +1 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import { fetchReblogs } from '../../actions/interactions';
|
||||
import { fetchStatus } from '../../actions/statuses';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { makeGetStatus } from '../../selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const getStatus = makeGetStatus();
|
||||
const status = getStatus(state, {
|
||||
id: props.params.statusId,
|
||||
username: props.params.username
|
||||
});
|
||||
|
||||
return {
|
||||
status,
|
||||
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
|
||||
}
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Reblogs extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
status: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchReblogs(nextProps.params.statusId));
|
||||
this.props.dispatch(fetchStatus(nextProps.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, status } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<Column>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this gab yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='reblogs'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './reblogs';
|
||||
72
app/javascript/gabsocial/features/reblogs/reblogs.js
Normal file
72
app/javascript/gabsocial/features/reblogs/reblogs.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import { fetchReblogs } from '../../actions/interactions';
|
||||
import { fetchStatus } from '../../actions/statuses';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../../components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { makeGetStatus } from '../../selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const getStatus = makeGetStatus();
|
||||
const status = getStatus(state, {
|
||||
id: props.params.statusId,
|
||||
username: props.params.username,
|
||||
});
|
||||
|
||||
return {
|
||||
status,
|
||||
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Reblogs extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
status: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchReblogs(this.props.params.statusId));
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||
this.props.dispatch(fetchReblogs(nextProps.params.statusId));
|
||||
this.props.dispatch(fetchStatus(nextProps.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, status } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
} else if (!status) {
|
||||
return (<ColumnIndicator type='missing' />);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this gab yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<ScrollableList
|
||||
scrollKey='reblogs'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withNote={false} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Toggle from 'react-toggle';
|
||||
import noop from 'lodash/noop';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import { MediaGallery, Video } from '../../ui/util/async-components';
|
||||
import Bundle from '../../ui/components/bundle';
|
||||
|
||||
export default class StatusCheckBox extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, checked, onToggle, disabled } = this.props;
|
||||
let media = null;
|
||||
|
||||
if (status.get('reblog')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
width={239}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='status-check-box'>
|
||||
<div className='status-check-box__status'>
|
||||
<StatusContent status={status} />
|
||||
{media}
|
||||
</div>
|
||||
|
||||
<div className='status-check-box-toggle'>
|
||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import StatusCheckBox from '../components/status_check_box';
|
||||
import { toggleStatusReport } from '../../../actions/reports';
|
||||
import { Set as ImmutableSet } from 'immutable';
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: state.getIn(['statuses', id]),
|
||||
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
|
||||
onToggle (e) {
|
||||
dispatch(toggleStatusReport(id, e.target.checked));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
||||
@@ -8,6 +8,7 @@ const mapStateToProps = state => ({
|
||||
submitted: state.getIn(['search', 'submitted']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -46,24 +47,10 @@ class Header extends ImmutablePureComponent {
|
||||
<NavLink to='/search' activeClassName='active'>
|
||||
<FormattedMessage id='search_results.top' defaultMessage='Top' />
|
||||
</NavLink>
|
||||
{/*<NavLink to='/search/gabs' activeClassName='active'>
|
||||
<FormattedMessage id='search_results.statuses' defaultMessage='Gabs' />
|
||||
</NavLink>
|
||||
<NavLink to='/search/people' activeClassName='active'>
|
||||
<FormattedMessage id='search_results.accounts' defaultMessage='People' />
|
||||
</NavLink>
|
||||
<NavLink to='/search/hashtags' activeClassName='active'>
|
||||
<FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' />
|
||||
</NavLink>
|
||||
<NavLink to='/search/groups' activeClassName='active'>
|
||||
<FormattedMessage id='search_results.groups' defaultMessage='Groups' />
|
||||
</NavLink>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Header);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.search-header {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
&__text-container {
|
||||
display: none;
|
||||
padding: 25px 0;
|
||||
background-color: lighten($ui-base-color, 6%);
|
||||
|
||||
@media (min-width:895px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
color: $primary-text-color;
|
||||
font-size: 27px;
|
||||
font-weight: bold;
|
||||
line-height: 32px;
|
||||
overflow: hidden;
|
||||
padding-left: 20px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__type-filters-tabs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media screen and (max-width:895px) {
|
||||
max-width: 580px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:895px) and (max-width:1190px) {
|
||||
|
||||
&__title-text,
|
||||
&__type-filters-tabs {
|
||||
max-width: 900px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './header';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user