Another 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 group page components.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { default } from './media_item';
|
||||
@@ -1,6 +1,6 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from '../../../components/icon';
|
||||
import Icon from '../../../../components/icon';
|
||||
import { autoPlayGif, displayMedia } from 'gabsocial/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
@@ -142,7 +142,7 @@ export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
return (
|
||||
<div className='account-gallery__item' style={{ width, height }}>
|
||||
<a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<a href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} />
|
||||
{visible && thumbnail}
|
||||
{!visible && icon}
|
||||
@@ -0,0 +1,17 @@
|
||||
.account-gallery__item {
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 2px;
|
||||
|
||||
&__icons {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { NavLink } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { me } from 'gabsocial/initial_state';
|
||||
|
||||
const mapStateToProps = (state, { params: { username }, withReplies = false }) => {
|
||||
const mapStateToProps = (state, { params: { username } }) => {
|
||||
const accounts = state.getIn(['accounts']);
|
||||
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() == username.toLowerCase());
|
||||
|
||||
@@ -85,7 +85,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { params: { username }, accountId, withReplies } = this.props;
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import InnerHeader from './inner_header';
|
||||
import MovedNote from './moved_note';
|
||||
import InnerHeader from '../inner_header';
|
||||
import MovedNote from '../moved_note';
|
||||
|
||||
import './header.scss';
|
||||
|
||||
export default class Header extends ImmutablePureComponent {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.account-timeline {
|
||||
&__header {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './header';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './inner_header.scss';
|
||||
@@ -10,7 +10,9 @@ 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';
|
||||
import ProfileInfoPanel from './profile_info_panel/profile_info_panel';
|
||||
|
||||
import './inner_header.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
@@ -0,0 +1,17 @@
|
||||
.relationship-tag {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
vertical-align: top;
|
||||
color: $primary-text-color;
|
||||
background-color: $base-overlay-background;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import AvatarOverlay from '../../../components/avatar_overlay';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export default class MovedNote extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
from: ImmutablePropTypes.map.isRequired,
|
||||
to: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { from, to } = this.props;
|
||||
const displayNameHtml = { __html: from.get('display_name_html') };
|
||||
|
||||
return (
|
||||
<div className='account__moved-note'>
|
||||
<div className='account__moved-note__message'>
|
||||
<div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
|
||||
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
|
||||
</div>
|
||||
|
||||
<NavLink to={`/${this.props.to.get('acct')}`} className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
||||
<DisplayName account={to} />
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './moved_note';
|
||||
@@ -0,0 +1,51 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import AvatarOverlay from '../../../../components/avatar_overlay';
|
||||
import DisplayName from '../../../../components/display_name';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
import './moved_note.scss';
|
||||
|
||||
export default class MovedNote extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
from: ImmutablePropTypes.map.isRequired,
|
||||
to: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { from, to } = this.props;
|
||||
const displayNameHtml = { __html: from.get('display_name_html') };
|
||||
|
||||
return (
|
||||
<div className='moved-note'>
|
||||
<div className='moved-note__message'>
|
||||
<div className='moved-note__icon-wrapper'>
|
||||
<Icon id='suitcase' className='moved-note__icon' fixedWidth />
|
||||
</div>
|
||||
<FormattedMessage
|
||||
id='account.moved_to'
|
||||
defaultMessage='{name} has moved to:'
|
||||
values={{
|
||||
name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NavLink to={`/${this.props.to.get('acct')}`} className='moved-note__display-name'>
|
||||
<div className='moved-note__display-avatar'>
|
||||
<AvatarOverlay account={to} friend={from} />
|
||||
</div>
|
||||
<DisplayName account={to} />
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.moved-note {
|
||||
padding: 14px 10px 16px 10px;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
&__message {
|
||||
position: relative;
|
||||
margin-left: 58px;
|
||||
color: $dark-text-color;
|
||||
padding-bottom: 4px;
|
||||
font-size: 14px;
|
||||
|
||||
>span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__display-name {
|
||||
display: block;
|
||||
color: $secondary-text-color;
|
||||
margin-bottom: 0;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
&:hover strong {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__display-avatar {
|
||||
position: relative;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './profile_info_panel';
|
||||
@@ -1,16 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Icon from 'gabsocial/components/icon';
|
||||
import VerifiedIcon from 'gabsocial/components/verified_icon';
|
||||
import Badge from 'gabsocial/components/badge';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import Icon from '../../../../components/icon';
|
||||
import VerifiedIcon from '../../../../components/verified_icon';
|
||||
import Badge from '../../../../components/badge';
|
||||
|
||||
import './profile_info_panel.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||
bot: { id: 'account.badges.bot', defaultMessage: 'Bot' },
|
||||
memberSince: { id: 'account.member_since', defaultMessage:'Member since {date}'},
|
||||
});
|
||||
|
||||
const dateFormatOptions = {
|
||||
@@ -60,7 +62,7 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
const lockedIcon = account.get('locked') ? (<Icon id='lock' title={intl.formatMessage(messages.account_locked)} />) : '';
|
||||
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
||||
const badge = account.get('bot') ? (<div className='account-role bot'>{intl.formatMessage(messages.bot)}</div>) : null;
|
||||
const content = { __html: account.get('note_emojified') };
|
||||
const fields = account.get('fields');
|
||||
const acct = account.get('acct');
|
||||
@@ -86,9 +88,9 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||
{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={{
|
||||
{intl.formatMessage(messages.memberSince, {
|
||||
date: memberSinceDate
|
||||
}} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -131,4 +133,4 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
.profile-info-panel {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1;
|
||||
|
||||
@media (min-width:895px) {
|
||||
padding-top: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.profile-info-panel-content {
|
||||
display: flex;
|
||||
|
||||
&__badges {
|
||||
display: flex;
|
||||
margin: 5px 0;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__join-date {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
|
||||
.fa {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $primary-text-color;
|
||||
|
||||
body.theme-gabsocial-light & {
|
||||
color: $gab-default-text-light;
|
||||
}
|
||||
|
||||
font-size: 15px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
|
||||
.account-role {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.emojione {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
span:first-of-type {
|
||||
color: #ffffff;
|
||||
|
||||
@include text-overflow;
|
||||
@include text-sizing(20px, 600, 1.25);
|
||||
|
||||
body.theme-gabsocial-light & {
|
||||
color: $gab-default-text-light;
|
||||
}
|
||||
}
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: $secondary-text-color;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@include text-sizing(16px, 400, 1.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bio {
|
||||
display: block;
|
||||
flex: 1 1;
|
||||
color: $primary-text-color;
|
||||
margin: 15px 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.25;
|
||||
|
||||
a {
|
||||
color: lighten($ui-highlight-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 1px solid lighten($ui-base-color, 12%);
|
||||
padding: 10px 0;
|
||||
margin: 5px 0;
|
||||
|
||||
@media screen and (max-width:895px) {
|
||||
border-bottom: 1px solid lighten($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
a {
|
||||
color: lighten($ui-highlight-color, 8%);
|
||||
}
|
||||
|
||||
dl:first-child .verified {
|
||||
border-radius: 0 4px 0 0;
|
||||
}
|
||||
|
||||
.verified a {
|
||||
color: $valid-value-color;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
padding: 2px 0;
|
||||
margin: 2px 0;
|
||||
flex: 1 1;
|
||||
|
||||
* {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
dt {
|
||||
min-width: 26px;
|
||||
}
|
||||
|
||||
dd {
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,15 +44,13 @@ class Blocks extends ImmutablePureComponent {
|
||||
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}
|
||||
emptyMessage={<FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../../components/setting_toggle';
|
||||
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)
|
||||
@injectIntl
|
||||
class ColumnSettings extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
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' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../../../components/setting_toggle';
|
||||
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)
|
||||
class ColumnSettings extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './column_settings';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { meUsername } from 'gabsocial/initial_state';
|
||||
import { openModal } from '../../../../actions/modal';
|
||||
import DropdownMenuContainer from '../../../../containers/dropdown_menu_container';
|
||||
import { meUsername } from '../../../../initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
||||
@@ -33,7 +33,7 @@ class ActionBar extends PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, onOpenHotkeys } = this.props;
|
||||
const { intl } = this.props;
|
||||
const size = this.props.size || 16;
|
||||
|
||||
let menu = [];
|
||||
@@ -51,8 +51,8 @@ class ActionBar extends PureComponent {
|
||||
menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', isLogout: true });
|
||||
|
||||
return (
|
||||
<div className='compose__action-bar' style={{'marginTop':'-6px'}}>
|
||||
<div className='compose__action-bar-dropdown'>
|
||||
<div style={{'marginTop':'-6px'}}>
|
||||
<div>
|
||||
<DropdownMenuContainer items={menu} icon='chevron-down' size={size} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './action_bar';
|
||||
@@ -1,37 +0,0 @@
|
||||
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';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
|
||||
return (
|
||||
<div className='autosuggest-account' title={account.get('acct')}>
|
||||
<div className='autosuggest-account-icon'>
|
||||
<Avatar account={account} size={18} />
|
||||
</div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { length } from 'stringz';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './character_counter.scss';
|
||||
|
||||
export default class CharacterCounter extends PureComponent {
|
||||
|
||||
@@ -7,17 +10,16 @@ export default class CharacterCounter extends PureComponent {
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
checkRemainingText (diff) {
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
}
|
||||
|
||||
render () {
|
||||
const diff = this.props.max - length(this.props.text);
|
||||
return this.checkRemainingText(diff);
|
||||
|
||||
const classes = classNames('character-counter', {
|
||||
'character-counter--over': (diff < 0),
|
||||
});
|
||||
|
||||
<div className='character-counter__wrapper'>
|
||||
<span className={classes}>{diff}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.character-counter {
|
||||
cursor: default;
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
color: $gab-secondary-text;
|
||||
|
||||
@include text-sizing(14px, 600);
|
||||
|
||||
&--over {
|
||||
color: $warning-red;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './character_counter';
|
||||
@@ -3,21 +3,23 @@ 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 AutosuggestTextbox from '../../../components/autosuggest_textbox';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
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 UploadForm from './upload_form';
|
||||
import WarningContainer from '../containers/warning_container';
|
||||
import { isMobile } from '../../../utils/is_mobile';
|
||||
import { countableText } from '../util/counter';
|
||||
import Icon from '../../../components/icon';
|
||||
import Button from '../../../components/button';
|
||||
import CharacterCounter from '../character_counter';
|
||||
import ReplyIndicatorContainer from '../../containers/reply_indicator_container';
|
||||
import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
|
||||
import PollButtonContainer from '../../containers/poll_button_container';
|
||||
import UploadButtonContainer from '../../containers/upload_button_container';
|
||||
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 UploadForm from '../upload_form/upload_form';
|
||||
import WarningContainer from '../../containers/warning_container';
|
||||
import { isMobile } from '../../../../utils/is_mobile';
|
||||
import { countableText } from '../../util/counter';
|
||||
import Icon from '../../../../components/icon';
|
||||
import Button from '../../../../components/button';
|
||||
|
||||
import './compose_form.scss';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
const maxPostCharacterCount = 3000;
|
||||
@@ -278,7 +280,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
<PrivacyDropdownContainer />
|
||||
<SpoilerButtonContainer />
|
||||
</div>
|
||||
<div className='character-counter__wrapper'><CharacterCounter max={maxPostCharacterCount} text={text} /></div>
|
||||
<CharacterCounter max={maxPostCharacterCount} text={text} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './compose_form';
|
||||
@@ -1,10 +1,12 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||
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 '../../../components/emoji/emoji';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { EmojiPicker as EmojiPickerAsync } from '../../../ui/util/async-components';
|
||||
import { buildCustomEmojis } from '../../../../components/emoji/emoji';
|
||||
|
||||
import './emoji_picker_dropdown.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
@@ -91,12 +93,24 @@ class ModifierPickerMenu extends PureComponent {
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||
<button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
|
||||
<button onClick={this.handleClick} data-index={1}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} />
|
||||
</button>
|
||||
<button onClick={this.handleClick} data-index={2}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} />
|
||||
</button>
|
||||
<button onClick={this.handleClick} data-index={3}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} />
|
||||
</button>
|
||||
<button onClick={this.handleClick} data-index={4}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} />
|
||||
</button>
|
||||
<button onClick={this.handleClick} data-index={5}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} />
|
||||
</button>
|
||||
<button onClick={this.handleClick} data-index={6}>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
.emoji-picker-dropdown__menu {
|
||||
background: $simple-background-color;
|
||||
position: absolute;
|
||||
box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
z-index: 20000;
|
||||
|
||||
.emoji-mart-scroll {
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
&.selecting .emoji-mart-scroll {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-picker-dropdown__modifiers {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emoji-picker-dropdown__modifiers__menu {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
top: -4px;
|
||||
left: -8px;
|
||||
background: $simple-background-color;
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
|
||||
overflow: hidden;
|
||||
|
||||
button {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
padding: 4px 8px;
|
||||
background: transparent;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: rgba($ui-secondary-color, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-mart-emoji {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-button {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
margin-left: 2px;
|
||||
width: 24px;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
img {
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.8;
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
img {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './emoji_picker_dropdown';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './navigation_bar';
|
||||
@@ -1,11 +1,13 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ActionBar from './action_bar';
|
||||
import Avatar from '../../../components/avatar';
|
||||
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';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ActionBar from '../action_bar';
|
||||
import Avatar from '../../../../components/avatar';
|
||||
import Permalink from '../../../../components/permalink';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import { me } from '../../../../initial_state';
|
||||
|
||||
import './navigation_bar.scss';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
@@ -22,16 +24,18 @@ class NavigationBar extends ImmutablePureComponent {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/${this.props.account.get('acct')}`}>
|
||||
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span>
|
||||
<Avatar account={this.props.account} size={48} />
|
||||
<Permalink href={account.get('url')} to={`/${account.get('acct')}`}>
|
||||
<span style={{ display: 'none' }}>{account.get('acct')}</span>
|
||||
<Avatar account={account} size={48} />
|
||||
</Permalink>
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
<Permalink href={this.props.account.get('url')} to={`/${this.props.account.get('acct')}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong>
|
||||
<Permalink href={account.get('url')} to={`/${account.get('acct')}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{account.get('acct')}</strong>
|
||||
</Permalink>
|
||||
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'>
|
||||
@@ -41,7 +45,7 @@ class NavigationBar extends ImmutablePureComponent {
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
|
||||
<ActionBar account={this.props.account} />
|
||||
<ActionBar account={account} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,89 @@
|
||||
.navigation-bar {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
cursor: default;
|
||||
color: $darker-text-color;
|
||||
|
||||
strong {
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.permalink {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
position: relative;
|
||||
|
||||
.icon-button.close {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
transform: scale(0.0, 1.0) translate(-100%, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.compose__action-bar .icon-button {
|
||||
pointer-events: auto;
|
||||
transform: scale(1.0, 1.0) translate(0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__profile {
|
||||
flex: 1 1 auto;
|
||||
margin-left: 8px;
|
||||
line-height: 20px;
|
||||
margin-top: -1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__profile-account {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
|
||||
@include text-overflow;
|
||||
}
|
||||
|
||||
&__profile-edit {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 630px) and (max-height: 400px) {
|
||||
$duration: 400ms;
|
||||
$delay: 100ms;
|
||||
|
||||
will-change: padding-bottom;
|
||||
transition: padding-bottom $duration $delay;
|
||||
|
||||
&>a:first-child {
|
||||
will-change: margin-top, margin-left, margin-right, width;
|
||||
transition: margin-top $duration $delay, margin-left $duration ($duration + $delay), margin-right $duration ($duration + $delay);
|
||||
}
|
||||
|
||||
&__profile-edit {
|
||||
will-change: margin-top;
|
||||
transition: margin-top $duration $delay;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
>.icon-button.close {
|
||||
will-change: opacity transform;
|
||||
transition: opacity $duration * 0.5 $delay,
|
||||
transform $duration $delay;
|
||||
}
|
||||
|
||||
.compose__action-bar .icon-button {
|
||||
will-change: opacity transform;
|
||||
transition: opacity $duration * 0.5 $delay + $duration * 0.5,
|
||||
transform $duration $delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './poll_button';
|
||||
@@ -1,4 +1,4 @@
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -35,13 +35,13 @@ class PollButton extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-button'>
|
||||
<div>
|
||||
<IconButton
|
||||
icon='tasks'
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
className={`compose-form__poll-button-icon ${active ? 'active' : ''}`}
|
||||
className={`${active ? 'active' : ''}`}
|
||||
size={18}
|
||||
inverted
|
||||
style={iconStyle}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './poll_form';
|
||||
@@ -2,9 +2,11 @@ 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 '../../../components/icon_button';
|
||||
import Icon from '../../../components/icon';
|
||||
import AutosuggestTextbox from '../../../components/autosuggest_textbox';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import Icon from '../../../../components/icon';
|
||||
import AutosuggestTextbox from '../../../../components/autosuggest_textbox';
|
||||
|
||||
import './poll_form.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
||||
@@ -0,0 +1,10 @@
|
||||
.poll-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__text {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - (23px + 6px));
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './privacy_dropdown';
|
||||
@@ -3,9 +3,11 @@ import spring from 'react-motion/lib/spring';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import classNames from 'classnames';
|
||||
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';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import Motion from '../../../ui/util/optional_motion';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
import './privacy_dropdown.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
@@ -0,0 +1,101 @@
|
||||
.privacy-dropdown {
|
||||
|
||||
&__dropdown {
|
||||
position: absolute;
|
||||
background: $simple-background-color;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 4px;
|
||||
margin-left: 40px;
|
||||
overflow: hidden;
|
||||
z-index: 10000;
|
||||
|
||||
&.top {
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
transform-origin: 50% 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
color: $inverted-text-color;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: $ui-highlight-color;
|
||||
color: $primary-text-color;
|
||||
outline: 0;
|
||||
|
||||
.privacy-dropdown__option__content {
|
||||
color: $primary-text-color;
|
||||
|
||||
strong {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active:hover {
|
||||
background: lighten($ui-highlight-color, 4%);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 10px;
|
||||
|
||||
@include flex(center, center);
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
color: $lighter-text-color;
|
||||
|
||||
strong {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
color: $inverted-text-color;
|
||||
|
||||
@each $lang in $cjk-langs {
|
||||
&:lang(#{$lang}) {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active & {
|
||||
&__dropdown {
|
||||
display: block;
|
||||
box-shadow: 2px 4px 6px rgba($base-shadow-color, 0.1);
|
||||
}
|
||||
|
||||
|
||||
&__value {
|
||||
background: $simple-background-color;
|
||||
border-radius: 4px 4px 0 0;
|
||||
box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1);
|
||||
|
||||
.icon-button {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $ui-highlight-color;
|
||||
|
||||
.icon-button {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active.top & {
|
||||
&__value {
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './reply_indicator';
|
||||
@@ -1,11 +1,13 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { isRtl } from '../../../utils/rtl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import Avatar from '../../../../components/avatar';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import DisplayName from '../../../../components/display_name';
|
||||
import { isRtl } from '../../../../utils/rtl';
|
||||
|
||||
import './reply_indicator.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
@@ -36,22 +38,26 @@ class ReplyIndicator extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
const style = {
|
||||
const style = {
|
||||
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted /></div>
|
||||
<div className='reply-indicator__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted />
|
||||
</div>
|
||||
|
||||
<NavLink to={`/${status.getIn(['account', 'acct'])}`} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
<div className='reply-indicator__display-avatar'>
|
||||
<Avatar account={status.get('account')} size={24} />
|
||||
</div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
|
||||
<div className='reply-indicator-content' style={style} dangerouslySetInnerHTML={content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
.reply-indicator {
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
background: $gab-background-base-light;
|
||||
padding: 10px;
|
||||
min-height: 23px;
|
||||
overflow-y: auto;
|
||||
flex: 0 2 auto;
|
||||
max-height: 500px;
|
||||
|
||||
@media screen and (min-width: 320px) and (max-width: 375px) {
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
max-height: 130px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin-bottom: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__cancel {
|
||||
float: right;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__display-name {
|
||||
color: $inverted-text-color;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
padding-right: 25px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover strong {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&__display-avatar {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-indicator-content {
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-top: 2px;
|
||||
color: $primary-text-color;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.status__content--with-spoiler {
|
||||
white-space: normal;
|
||||
|
||||
.status__content__text {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.emojione {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -3px 0 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gab-brand-default;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
.fa {
|
||||
color: lighten($dark-text-color, 7%);
|
||||
}
|
||||
}
|
||||
|
||||
&.mention {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fa {
|
||||
color: $dark-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content__spoiler-link {
|
||||
background: $action-button-color;
|
||||
|
||||
&:hover {
|
||||
background: lighten($action-button-color, 7%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.status__content__text {
|
||||
display: none;
|
||||
|
||||
&.status__content__text--visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,12 @@
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import { searchEnabled } from '../../../initial_state';
|
||||
import Icon from '../../../components/icon';
|
||||
import SearchPopout from '../../../components/search_popout';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
class SearchPopout extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { style } = this.props;
|
||||
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favorited, reposted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
|
||||
return (
|
||||
<div className='search-popout-container' style={{ ...style, position: 'absolute', zIndex: 1000 }}>
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
|
||||
<ul>
|
||||
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
|
||||
<li><em>@username</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
|
||||
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
|
||||
</ul>
|
||||
{extraInformation}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class Search extends PureComponent {
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './search_results';
|
||||
@@ -1,18 +1,18 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
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 TrendingItem from '../../../components/trending_item';
|
||||
import Icon from '../../../components/icon';
|
||||
import WhoToFollowPanel from '../../../components/panel';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../../../containers/account_container';
|
||||
import StatusContainer from '../../../../containers/status_container';
|
||||
import TrendingItem from '../../../../components/trending_item';
|
||||
import Icon from '../../../../components/icon';
|
||||
import WhoToFollowPanel from '../../../../components/panel';
|
||||
|
||||
export default @injectIntl
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
import './search_results.scss';
|
||||
|
||||
export default class SearchResults extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -20,14 +20,13 @@ class SearchResults extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, results, dismissSuggestion } = this.props;
|
||||
const { results } = this.props;
|
||||
const { isSmallScreen } = this.state;
|
||||
|
||||
if (results.isEmpty() && isSmallScreen) {
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<WhoToFollowPanel />
|
||||
{/* <TrendsPanel /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -36,10 +35,13 @@ class SearchResults extends ImmutablePureComponent {
|
||||
let count = 0;
|
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
count += results.get('accounts').size;
|
||||
count += results.get('accounts').size;
|
||||
accounts = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
|
||||
<h5>
|
||||
<Icon id='users' fixedWidth />
|
||||
<FormattedMessage id='search_results.accounts' defaultMessage='People' />
|
||||
</h5>
|
||||
|
||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
</div>
|
||||
@@ -47,10 +49,13 @@ class SearchResults extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
count += results.get('statuses').size;
|
||||
count += results.get('statuses').size;
|
||||
statuses = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Gabs' /></h5>
|
||||
<h5>
|
||||
<Icon id='quote-right' fixedWidth />
|
||||
<FormattedMessage id='search_results.statuses' defaultMessage='Gabs' />
|
||||
</h5>
|
||||
|
||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
</div>
|
||||
@@ -72,7 +77,13 @@ class SearchResults extends ImmutablePureComponent {
|
||||
<div className='search-results'>
|
||||
<div className='search-results__header'>
|
||||
<Icon id='search' fixedWidth />
|
||||
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} />
|
||||
<FormattedMessage
|
||||
id='search_results.total'
|
||||
defaultMessage='{count, number} {count, plural, one {result} other {results}}'
|
||||
values={{
|
||||
count
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{accounts}
|
||||
@@ -0,0 +1,40 @@
|
||||
.search-results {
|
||||
&__header {
|
||||
color: $dark-text-color;
|
||||
background: lighten($ui-base-color, 2%);
|
||||
padding: 15px;
|
||||
cursor: default;
|
||||
|
||||
@include text-sizing(16px, 500);
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__section {
|
||||
margin-bottom: 5px;
|
||||
|
||||
h5 {
|
||||
display: flex;
|
||||
background: darken($ui-base-color, 4%);
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
color: $dark-text-color;
|
||||
padding: 15px;
|
||||
cursor: default;
|
||||
|
||||
@include text-sizing(16px, 500);
|
||||
|
||||
.fa {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.account:last-child,
|
||||
&>div:last-child .status {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './text_icon_button';
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -18,7 +17,14 @@ export default class TextIconButton extends PureComponent {
|
||||
const { label, title, active, ariaControls } = this.props;
|
||||
|
||||
return (
|
||||
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}>
|
||||
<button
|
||||
title={title}
|
||||
aria-label={title}
|
||||
className={`text-icon-button ${active ? 'text-icon-button--active' : ''}`}
|
||||
aria-expanded={active}
|
||||
onClick={this.handleClick}
|
||||
aria-controls={ariaControls}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
.text-icon-button {
|
||||
color: $gab-secondary-text;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
outline: 0;
|
||||
transition: color 100ms ease-in;
|
||||
|
||||
@include text-sizing(11px, 600, 27px);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: darken($lighter-text-color, 7%);
|
||||
transition: color 200ms ease-out;
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-moz-focus-inner,
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: 0 !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './upload';
|
||||
@@ -1,13 +1,17 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import Icon from '../../../components/icon';
|
||||
import Motion from '../../../ui/util/optional_motion';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
|
||||
import './upload.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
|
||||
undo: { id: 'upload_form.undo', defaultMessage: 'Delete' },
|
||||
focus: { id: 'upload_form.focus', defaultMessage: 'Crop' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@@ -93,16 +97,30 @@ class Upload extends ImmutablePureComponent {
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
||||
<div className='compose-form-upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
|
||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ scale }) => (
|
||||
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||
<div className={classNames('compose-form__upload__actions', { active })}>
|
||||
<button className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||
{media.get('type') === 'image' && <button className='icon-button' onClick={this.handleFocalPointClick}><Icon id='crosshairs' /> <FormattedMessage id='upload_form.focus' defaultMessage='Crop' /></button>}
|
||||
<div className='compose-form-upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
|
||||
<div className={classNames('compose-form-upload__actions', { active })}>
|
||||
<IconButton
|
||||
onClick={this.handleUndoClick}
|
||||
icon='times'
|
||||
title={intl.formatMessage(messages.undo)}
|
||||
text={intl.formatMessage(messages.undo)}
|
||||
/>
|
||||
|
||||
{
|
||||
media.get('type') === 'image' &&
|
||||
<IconButton
|
||||
onClick={this.handleFocalPointClick}
|
||||
icon='crosshairs'
|
||||
title={intl.formatMessage(messages.focus)}
|
||||
text={intl.formatMessage(messages.focus)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={classNames('compose-form__upload-description', { active })}>
|
||||
<div className={classNames('compose-form-upload__description', { active })}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
.compose-form-upload {
|
||||
flex: 1 1 0;
|
||||
min-width: 40%;
|
||||
margin: 5px;
|
||||
|
||||
&__actions {
|
||||
background: linear-gradient(180deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
|
||||
opacity: 0;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
@include flex(space-between, flex-start);
|
||||
|
||||
.icon-button {
|
||||
flex: 0 1 auto;
|
||||
color: $gab-secondary-text;
|
||||
padding: 10px;
|
||||
font-family: inherit;
|
||||
|
||||
@include text-sizing(14px, 500);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: $gab-text-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent);
|
||||
padding: 10px;
|
||||
opacity: 0;
|
||||
transition: opacity .1s ease;
|
||||
|
||||
@include abs-position(auto, 0, 0, 0);
|
||||
|
||||
textarea {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
background: transparent;
|
||||
color: $gab-secondary-text;
|
||||
border: 1px solid $gab-secondary-text;
|
||||
outline: none;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
|
||||
@include text-sizing(14px, 500);
|
||||
|
||||
&:focus {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $gab-secondary-text;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__thumbnail {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
@include size(100%, 140px);
|
||||
@include background-image;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './upload_button';
|
||||
@@ -1,7 +1,9 @@
|
||||
import IconButton from '../../../components/icon_button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
|
||||
import './upload_button.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' },
|
||||
@@ -56,8 +58,17 @@ class UploadButton extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<div className='compose-form-upload-button'>
|
||||
<IconButton
|
||||
inverted
|
||||
icon='camera'
|
||||
title={intl.formatMessage(messages.upload)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
className='compose-form-upload-button__icon'
|
||||
size={18}
|
||||
style={iconStyle}
|
||||
/>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
|
||||
<input
|
||||
@@ -0,0 +1,5 @@
|
||||
.compose-form-upload-button {
|
||||
&__icon {
|
||||
line-height: 27px;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
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';
|
||||
import UploadProgress from '../upload_progress';
|
||||
import UploadContainer from '../../containers/upload_container';
|
||||
import SensitiveButtonContainer from '../../containers/sensitive_button_container';
|
||||
|
||||
import './upload_form.scss';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
mediaIds: state.getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
||||
@@ -19,10 +21,10 @@ class UploadForm extends ImmutablePureComponent {
|
||||
const { mediaIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-wrapper'>
|
||||
<div className='compose-form-upload-wrapper'>
|
||||
<UploadProgress />
|
||||
|
||||
<div className='compose-form__uploads-wrapper'>
|
||||
<div className='compose-form-uploads-wrapper'>
|
||||
{mediaIds.map(id => (
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
@@ -0,0 +1,10 @@
|
||||
.compose-form-upload-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compose-form-uploads-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './upload_progress';
|
||||
@@ -1,7 +1,9 @@
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import Icon from '../../../components/icon';
|
||||
import Motion from '../../../ui/util/optional_motion';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
import './upload_progress.scss'
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'is_uploading']),
|
||||
@@ -0,0 +1,39 @@
|
||||
.upload-progress {
|
||||
padding: 10px;
|
||||
color: $lighter-text-color;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
.fa {
|
||||
font-size: 34px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
span {
|
||||
text-transform: uppercase;
|
||||
display: block;
|
||||
|
||||
@include text-sizing(12px, 500);
|
||||
}
|
||||
|
||||
&__message {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
border-radius: 6px;
|
||||
background: $ui-base-lighter-color;
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
|
||||
@include size(100%, 6px);
|
||||
}
|
||||
|
||||
&__tracker {
|
||||
height: 6px;
|
||||
background: $ui-highlight-color;
|
||||
border-radius: 6px;
|
||||
|
||||
@include abs-position(0, auto, auto, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './warning';
|
||||
@@ -1,6 +1,8 @@
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
import Motion from '../../../ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import './warning.scss';
|
||||
|
||||
export default class Warning extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
@@ -13,7 +15,7 @@ export default class Warning extends PureComponent {
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<div className='compose-form-warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
@@ -0,0 +1,33 @@
|
||||
.compose-form-warning {
|
||||
color: $inverted-text-color;
|
||||
margin-bottom: 10px;
|
||||
background: $ui-primary-color;
|
||||
box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3);
|
||||
padding: 8px 10px;
|
||||
border-radius: 4px;
|
||||
|
||||
@include text-sizing(13px, 400);
|
||||
|
||||
strong {
|
||||
color: $inverted-text-color;
|
||||
font-weight: 500;
|
||||
|
||||
@each $lang in $cjk-langs {
|
||||
&:lang(#{$lang}) {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $lighter-text-color;
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
104
app/javascript/gabsocial/features/compose/compose.js
Normal file
104
app/javascript/gabsocial/features/compose/compose.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
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 '../../components/icon';
|
||||
|
||||
import './compose.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new gab' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
showSearch: ownProps.isSearchPage,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Compose extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columns: ImmutablePropTypes.list.isRequired,
|
||||
showSearch: PropTypes.bool,
|
||||
isSearchPage: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { isSearchPage } = this.props;
|
||||
|
||||
if (!isSearchPage) {
|
||||
this.props.dispatch(mountCompose());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { isSearchPage } = this.props;
|
||||
|
||||
if (!isSearchPage) {
|
||||
this.props.dispatch(unmountCompose());
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.props.dispatch(changeComposing(true));
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.props.dispatch(changeComposing(false));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { showSearch, isSearchPage, intl } = this.props;
|
||||
|
||||
let header = '';
|
||||
|
||||
return (
|
||||
<div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
|
||||
{header}
|
||||
|
||||
{isSearchPage && <SearchContainer /> }
|
||||
|
||||
<div className='drawer__pager'>
|
||||
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationBar onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
|
||||
<div className='drawer__inner__gabsocial'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
<SearchResultsContainer />
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
107
app/javascript/gabsocial/features/compose/compose.scss
Normal file
107
app/javascript/gabsocial/features/compose/compose.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
.drawer {
|
||||
width: 300px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.drawer__tab {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
padding: 15px 5px 13px;
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
flex: 1 1 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.drawer__inner {
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: lighten($ui-base-color, 13%);
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.darker {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__inner__gabsocial {
|
||||
background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto;
|
||||
flex: 1;
|
||||
min-height: 47px;
|
||||
display: none;
|
||||
|
||||
>img {
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
object-position: bottom left;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media screen and (min-height: 640px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.drawer__header {
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
a {
|
||||
transition: background 100ms ease-in;
|
||||
|
||||
&:hover {
|
||||
background: lighten($ui-base-color, 3%);
|
||||
transition: background 200ms ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 631px) {
|
||||
.drawer {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
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 '../../components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
|
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new gab' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Compose extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columns: ImmutablePropTypes.list.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
showSearch: PropTypes.bool,
|
||||
isSearchPage: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { isSearchPage } = this.props;
|
||||
|
||||
if (!isSearchPage) {
|
||||
this.props.dispatch(mountCompose());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { isSearchPage } = this.props;
|
||||
|
||||
if (!isSearchPage) {
|
||||
this.props.dispatch(unmountCompose());
|
||||
}
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.props.dispatch(changeComposing(true));
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
this.props.dispatch(changeComposing(false));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { multiColumn, showSearch, isSearchPage, intl } = this.props;
|
||||
|
||||
let header = '';
|
||||
|
||||
if (multiColumn) {
|
||||
const { columns } = this.props;
|
||||
header = (
|
||||
<nav className='drawer__header'>
|
||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' fixedWidth /></Link>
|
||||
{!columns.some(column => column.get('id') === 'HOME') && (
|
||||
<Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
|
||||
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'COMMUNITY') && (
|
||||
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'PUBLIC') && (
|
||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
|
||||
)}
|
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
|
||||
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}>
|
||||
{header}
|
||||
|
||||
{(multiColumn || isSearchPage) && <SearchContainer /> }
|
||||
|
||||
<div className='drawer__pager'>
|
||||
{!isSearchPage && <div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationBar onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer />
|
||||
|
||||
<div className='drawer__inner__gabsocial'>
|
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Motion defaultStyle={{ x: isSearchPage ? 0 : -100 }} style={{ x: spring(showSearch || isSearchPage ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
<SearchResultsContainer />
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './compose';
|
||||
@@ -45,15 +45,13 @@ class Blocks extends ImmutablePureComponent {
|
||||
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}
|
||||
emptyMessage={<FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />}
|
||||
>
|
||||
{domains.map(domain =>
|
||||
<DomainContainer key={domain} domain={domain} />
|
||||
|
||||
@@ -43,8 +43,6 @@ class Favourites extends ImmutablePureComponent {
|
||||
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
|
||||
@@ -53,7 +51,7 @@ class Favourites extends ImmutablePureComponent {
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
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." />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ class AccountAuthorize extends ImmutablePureComponent {
|
||||
return (
|
||||
<div className='account-authorize__wrapper'>
|
||||
<div className='account-authorize'>
|
||||
<Permalink href={`/${account.get('acct')}`} to={`/${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<Permalink href={`/${account.get('acct')}`} to={`/${account.get('acct')}`} className='account-authorize__display-name'>
|
||||
<div className='account-authorize__avatar'>
|
||||
<Avatar account={account} size={48} />
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,53 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.detailed-status__display-name {
|
||||
&__display-name {
|
||||
display: block;
|
||||
color: $secondary-text-color;
|
||||
margin-bottom: 15px;
|
||||
line-height: 24px;
|
||||
overflow: hidden;
|
||||
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
&:hover strong {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
color: $darker-text-color;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
word-break: normal;
|
||||
word-wrap: break-word;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,15 +44,13 @@ class FollowRequests extends ImmutablePureComponent {
|
||||
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)} backBtn='slim'>
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
emptyMessage={emptyMessage}
|
||||
emptyMessage={<FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountAuthorize key={id} id={id} />
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import SettingToggle from '../../../components/setting_toggle';
|
||||
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)
|
||||
@injectIntl
|
||||
class ColumnSettings extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', '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' />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { changeSetting, saveSettings } from '../../../../actions/settings';
|
||||
import SettingToggle from '../../../../components/setting_toggle';
|
||||
import ColumnSettingsHeading from '../../../../components/column_settings_heading';
|
||||
|
||||
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)
|
||||
class ColumnSettings extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ColumnSettingsHeading
|
||||
heading={<FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' />}
|
||||
/>
|
||||
|
||||
<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', 'reply']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './column_settings';
|
||||
@@ -1,41 +0,0 @@
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Avatar from '../../../components/avatar';
|
||||
import DisplayName from '../../../components/display_name';
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
|
||||
export default @connect(makeMapStateToProps)
|
||||
@injectIntl
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './list';
|
||||
@@ -1,9 +1,11 @@
|
||||
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 { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
||||
import Icon from '../../../components/icon';
|
||||
import IconButton from '../../../../components/icon_button';
|
||||
import { removeFromListAdder, addToListAdder } from '../../../../actions/lists';
|
||||
import Icon from '../../../../components/icon';
|
||||
|
||||
import './list.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
@@ -39,24 +41,21 @@ class List extends ImmutablePureComponent {
|
||||
render () {
|
||||
const { list, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='list-ul' className='column-link__icon' fixedWidth />
|
||||
<div className='list__name'>
|
||||
<Icon id='list-ul' className='list__name-icon' fixedWidth />
|
||||
{list.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
<div className='list__btn-block'>
|
||||
{
|
||||
added ?
|
||||
<IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />
|
||||
:
|
||||
<IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
.list {
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
|
||||
.account__relationship {
|
||||
padding: 8px 5px 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__name-icon {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
&__btn-block {
|
||||
height: auto;
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import List from './components/list';
|
||||
import Account from './components/account';
|
||||
import IconButton from '../../components/icon_button';
|
||||
import NewListForm from '../lists/components/new_list_form';
|
||||
import ColumnSubheading from '../../components/column_subheading/column_subheading';
|
||||
// hack
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, {accountId}) => ({
|
||||
listIds: getOrderedLists(state).map(list=>list.get('id')),
|
||||
account: state.getIn(['accounts', accountId]),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupListAdder(accountId)),
|
||||
onReset: () => dispatch(resetListAdder()),
|
||||
});
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
add: { id: 'lists.new.create', defaultMessage: 'Add List' },
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ListAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
listIds: ImmutablePropTypes.list.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
onClickClose = () => {
|
||||
this.props.onClose('LIST_ADDER');
|
||||
};
|
||||
|
||||
render () {
|
||||
const { accountId, listIds, intl, onClose, account } = this.props;
|
||||
|
||||
const displayNameHtml = account ? { __html: account.get('display_name_html') } : '';
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal compose-modal'>
|
||||
<div className='compose-modal__header'>
|
||||
<h3 className='compose-modal__header__title'>
|
||||
<FormattedMessage id='list_adder.header_title' defaultMessage='Add or Remove from Lists' />
|
||||
</h3>
|
||||
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
|
||||
</div>
|
||||
<div className='compose-modal__content'>
|
||||
<div className='list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.add)} />
|
||||
<NewListForm />
|
||||
|
||||
<br/>
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<div className='list-adder__lists'>
|
||||
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './list_adder';
|
||||
99
app/javascript/gabsocial/features/list_adder/list_adder.js
Normal file
99
app/javascript/gabsocial/features/list_adder/list_adder.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import List from './components/list';
|
||||
import Account from '../../components/account';
|
||||
import IconButton from '../../components/icon_button';
|
||||
import NewListForm from '../lists/components/new_list_form';
|
||||
import ColumnSubheading from '../../components/column_subheading/column_subheading';
|
||||
|
||||
import './list_adder.scss';
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
listIds: getOrderedLists(state).map(list => list.get('id')),
|
||||
account: state.getIn(['accounts', accountId]),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupListAdder(accountId)),
|
||||
onReset: () => dispatch(resetListAdder()),
|
||||
});
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
add: { id: 'lists.new.create', defaultMessage: 'Add List' },
|
||||
headerTitle: { id: 'list_adder.header_title', defaultMessage: 'Add or Remove from Lists' },
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ListAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
listIds: ImmutablePropTypes.list.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
onClickClose = () => {
|
||||
this.props.onClose('LIST_ADDER');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { listIds, intl, account } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal compose-modal'>
|
||||
<div className='compose-modal__header'>
|
||||
<h3 className='compose-modal__header__title'>{intl.formatMessage(messages.headerTitle)}</h3>
|
||||
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
|
||||
</div>
|
||||
<div className='compose-modal__content'>
|
||||
<div className='list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account account={account} displayOnly/>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.add)} />
|
||||
<NewListForm />
|
||||
|
||||
<br />
|
||||
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<div className='list-adder__lists'>
|
||||
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
20
app/javascript/gabsocial/features/list_adder/list_adder.scss
Normal file
20
app/javascript/gabsocial/features/list_adder/list_adder.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.list-adder {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
@include size(100%);
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
&__account {
|
||||
background: lighten($ui-base-color, 13%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&__lists {
|
||||
background: lighten($ui-base-color, 13%);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,9 @@ class Account extends ImmutablePureComponent {
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||
import Button from '../../../components/button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../../actions/lists';
|
||||
import ColumnInlineForm from '../../../../components/column_inline_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||
@@ -29,42 +29,18 @@ class ListForm extends PureComponent {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage(messages.title);
|
||||
const save = intl.formatMessage(messages.save);
|
||||
const { value, disabled, intl, onSubmit, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text new-list-form__input'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
{ !disabled &&
|
||||
<Button
|
||||
className='new-list-form__btn'
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{save}
|
||||
</Button>
|
||||
}
|
||||
</form>
|
||||
<ColumnInlineForm
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
label={intl.formatMessage(messages.title)}
|
||||
btnTitle={intl.formatMessage(messages.save)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './edit_list_form';
|
||||
@@ -1,99 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Account from './components/account';
|
||||
import Search from './components/search';
|
||||
import EditListForm from './components/edit_list_form';
|
||||
import ColumnSubheading from '../../components/column_subheading';
|
||||
import IconButton from '../../components/icon_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onReset: () => dispatch(resetListEditor()),
|
||||
});
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
changeTitle: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||
addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
editList: { id: 'lists.edit', defaultMessage: 'Edit list' },
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, listId } = this.props;
|
||||
onInitialize(listId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
onClickClose = () => {
|
||||
this.props.onClose('LIST_ADDER');
|
||||
};
|
||||
|
||||
render () {
|
||||
const { accountIds, searchAccountIds, onClear, intl } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal compose-modal'>
|
||||
<div className='compose-modal__header'>
|
||||
<h3 className='compose-modal__header__title'>
|
||||
{intl.formatMessage(messages.editList)}
|
||||
</h3>
|
||||
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
|
||||
</div>
|
||||
<div className='compose-modal__content'>
|
||||
<div className='list-editor'>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.changeTitle)} />
|
||||
<EditListForm />
|
||||
<br/>
|
||||
|
||||
{
|
||||
accountIds.size > 0 &&
|
||||
<div>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.removeFromList)} />
|
||||
<div className='list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<br/>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.addToList)} />
|
||||
<Search />
|
||||
<div className='list-editor__accounts'>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './list_editor';
|
||||
98
app/javascript/gabsocial/features/list_editor/list_editor.js
Normal file
98
app/javascript/gabsocial/features/list_editor/list_editor.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Account from './components/account';
|
||||
import Search from './components/search';
|
||||
import EditListForm from './components/edit_list_form/edit_list_form';
|
||||
import ColumnSubheading from '../../components/column_subheading';
|
||||
import IconButton from '../../components/icon_button';
|
||||
|
||||
import './list_editor.scss';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
||||
onReset: () => dispatch(resetListEditor()),
|
||||
});
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
changeTitle: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||
addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
editList: { id: 'lists.edit', defaultMessage: 'Edit list' },
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { onInitialize, listId } = this.props;
|
||||
onInitialize(listId);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
onClickClose = () => {
|
||||
this.props.onClose('LIST_ADDER');
|
||||
};
|
||||
|
||||
render() {
|
||||
const { accountIds, searchAccountIds, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal compose-modal'>
|
||||
<div className='compose-modal__header'>
|
||||
<h3 className='compose-modal__header__title'>
|
||||
{intl.formatMessage(messages.editList)}
|
||||
</h3>
|
||||
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={this.onClickClose} size={20} />
|
||||
</div>
|
||||
<div className='compose-modal__content'>
|
||||
<div className='list-editor'>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.changeTitle)} />
|
||||
<EditListForm />
|
||||
<br />
|
||||
|
||||
{
|
||||
accountIds.size > 0 &&
|
||||
<div>
|
||||
<ColumnSubheading text={intl.formatMessage(messages.removeFromList)} />
|
||||
<div className='list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.addToList)} />
|
||||
<Search />
|
||||
<div className='list-editor__accounts'>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
.list-editor {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
@include size(100%);
|
||||
|
||||
@media screen and (max-width: 420px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding: 15px 0;
|
||||
background: lighten($ui-base-color, 13%);
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
@include text-sizing(16px, 500, 1, center);
|
||||
}
|
||||
|
||||
.drawer__inner {
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
&.backdrop {
|
||||
width: calc(100% - 60px);
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__accounts {
|
||||
background: lighten($ui-base-color, 13%);
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.account {
|
||||
&__display-name {
|
||||
&:hover strong {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 10px 0;
|
||||
|
||||
>label {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
||||
>.search__icon .fa {
|
||||
right: 102px !important;
|
||||
}
|
||||
|
||||
>.button {
|
||||
width: 80px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import ColumnIndicator from '../../components/column_indicator';
|
||||
import Icon from '../../components/icon';
|
||||
import HomeColumnHeader from '../../components/column_header';
|
||||
import Button from '../../components/button';
|
||||
import ColumnHeaderSettingButton from '../../components/column_header_setting_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
||||
@@ -106,30 +107,39 @@ class ListTimeline extends ImmutablePureComponent {
|
||||
|
||||
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>
|
||||
<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>
|
||||
<div>
|
||||
<ColumnHeaderSettingButton
|
||||
onClick={this.handleEditClick}
|
||||
icon='pencil'
|
||||
title={<FormattedMessage id='lists.edit' defaultMessage='Edit list' />}
|
||||
/>
|
||||
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
<ColumnHeaderSettingButton
|
||||
onClick={this.handleDeleteClick}
|
||||
icon='trash'
|
||||
title={<FormattedMessage id='lists.delete' defaultMessage='Delete list' />}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<ColumnHeaderSettingButton
|
||||
to='/lists'
|
||||
icon='arrow-right'
|
||||
title={<FormattedMessage id='lists.view_all' defaultMessage='View all lists' />}
|
||||
/>
|
||||
</div>
|
||||
</HomeColumnHeader>
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||
import Button from '../../../components/button';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
|
||||
title: { id: 'lists.new.create', defaultMessage: 'Add list' },
|
||||
create: { id: 'lists.new.create_title', defaultMessage: 'Create' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: state.getIn(['listEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(true)),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class NewListForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const title = intl.formatMessage(messages.title);
|
||||
const create = intl.formatMessage(messages.create);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text new-list-form__input'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
className='new-list-form__btn'
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{create}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './new_list_form';
|
||||
@@ -0,0 +1,47 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../../actions/lists';
|
||||
import ColumnInlineForm from '../../../../components/column_inline_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
|
||||
create: { id: 'lists.new.create_title', defaultMessage: 'Create' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: state.getIn(['listEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(true)),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class NewListForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl, onSubmit, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<ColumnInlineForm
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
label={intl.formatMessage(messages.label)}
|
||||
btnTitle={intl.formatMessage(messages.create)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +1 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
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 '../../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';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
add: { id: 'lists.new.create', defaultMessage: 'Add List' },
|
||||
});
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Lists extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this.props.dispatch(fetchLists());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, lists } = this.props;
|
||||
|
||||
if (!lists) {
|
||||
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)} backBtn='slim'>
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.add)} />
|
||||
<NewListForm />
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{lists.map(list =>
|
||||
<ColumnLink key={list.get('id')} to={`/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export { default } from './lists';
|
||||
77
app/javascript/gabsocial/features/lists/lists.js
Normal file
77
app/javascript/gabsocial/features/lists/lists.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { createSelector } from 'reselect';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ColumnIndicator from '../../components/column_indicator';
|
||||
import Column from '../../components/column';
|
||||
import { fetchLists } from '../../actions/lists';
|
||||
import ColumnLink from '../../components/column_link';
|
||||
import ColumnSubheading from '../../components/column_subheading';
|
||||
import NewListForm from './components/new_list_form';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
add: { id: 'lists.new.create', defaultMessage: 'Add List' },
|
||||
});
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Lists extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.dispatch(fetchLists());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, lists } = this.props;
|
||||
|
||||
if (!lists) {
|
||||
return (<ColumnIndicator type='loading' />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column icon='list-ul' heading={intl.formatMessage(messages.heading)} backBtn='slim'>
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.add)} />
|
||||
<NewListForm />
|
||||
<br />
|
||||
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
emptyMessage={<FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />}
|
||||
>
|
||||
{lists.map(list =>
|
||||
<ColumnLink
|
||||
key={list.get('id')}
|
||||
to={`/list/${list.get('id')}`}
|
||||
icon='list-ul'
|
||||
text={list.get('title')}
|
||||
/>
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,15 +44,13 @@ class Mutes extends ImmutablePureComponent {
|
||||
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}
|
||||
emptyMessage={<FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user