Large update for all components

reorganization, linting, updating file imports, consolidation
warning: there will be errors in this commit
todo: update webpack, add missing styles, scss files, consolidate the rest of components within features/*
This commit is contained in:
mgabdev 2019-08-07 01:02:36 -04:00
parent 5505f60119
commit 280dc51d85
341 changed files with 8876 additions and 8321 deletions

View File

@ -6,7 +6,7 @@ import {
importFetchedAccounts,
importErrorWhileFetchingAccountByUsername,
} from './importer';
import { me } from 'gabsocial/initial_state';
import { me } from '../initial_state';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';

View File

@ -1,7 +1,7 @@
import api from '../api';
import { CancelToken, isCancel } from 'axios';
import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { search as emojiSearch } from '../components/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
import resizeImage from '../utils/resize_image';

View File

@ -1,5 +1,5 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji';
import emojify from '../../components/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
import { expandSpoilers } from '../../initial_state';

View File

@ -22,10 +22,6 @@ export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
export const PIN_REQUEST = 'PIN_REQUEST';
export const PIN_SUCCESS = 'PIN_SUCCESS';
export const PIN_FAIL = 'PIN_FAIL';
@ -233,43 +229,6 @@ export function fetchReblogsFail(id, error) {
};
};
export function fetchFavourites(id) {
return (dispatch, getState) => {
if (!me) return;
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchFavouritesSuccess(id, response.data));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
});
};
};
export function fetchFavouritesRequest(id) {
return {
type: FAVOURITES_FETCH_REQUEST,
id,
};
};
export function fetchFavouritesSuccess(id, accounts) {
return {
type: FAVOURITES_FETCH_SUCCESS,
id,
accounts,
};
};
export function fetchFavouritesFail(id, error) {
return {
type: FAVOURITES_FETCH_FAIL,
error,
};
};
export function pin(status) {
return (dispatch, getState) => {
if (!me) return;

View File

@ -2,7 +2,7 @@ import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from './modal';
import { me } from 'gabsocial/initial_state';
import { me } from '../initial_state';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';

View File

@ -1,6 +1,6 @@
import api from '../api';
import { importFetchedAccounts } from './importer';
import { me } from 'gabsocial/initial_state';
import { me } from '../initial_state';
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';

View File

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
exports[`<Button /> adds class "button--secondary" if props.secondary given 1`] = `
<button
className="button button-secondary"
className="button button--secondary"
onClick={[Function]}
style={
Object {

View File

@ -65,7 +65,7 @@ describe('<Button />', () => {
expect(tree).toMatchSnapshot();
});
it('adds class "button-secondary" if props.secondary given', () => {
it('adds class "button--secondary" if props.secondary given', () => {
const component = renderer.create(<Button secondary />);
const tree = component.toJSON();

View File

@ -0,0 +1,128 @@
import { Fragment } from 'react';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Avatar from '../avatar/avatar';
import DisplayName from '../display_name';
import IconButton from '../icon_button';
import { me } from '../../initial_state';
import './account.scss';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
onActionClick: PropTypes.func,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
handleBlock = () => {
this.props.onBlock(this.props.account);
}
handleMute = () => {
this.props.onMute(this.props.account);
}
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
}
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
}
handleAction = () => {
this.props.onActionClick(this.props.account);
}
render() {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
if (!account) {
return <div />;
}
if (hidden) {
return (
<Fragment>
{account.get('display_name')}
{account.get('username')}
</Fragment>
);
}
let buttons;
if (onActionClick && actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<Fragment>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton}
</Fragment>
);
} else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}
}
return (
<div className='account'>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Link>
<div className='account__relationship'>
{buttons}
</div>
</div>
</div>
);
}
}

View File

@ -1,128 +1 @@
import { Fragment } from 'react';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Avatar from '../avatar';
import DisplayName from '../display_name';
import IconButton from '../icon_button';
import { me } from '../../initial_state';
import './index.scss';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
onActionClick: PropTypes.func,
};
handleFollow = () => {
this.props.onFollow(this.props.account);
}
handleBlock = () => {
this.props.onBlock(this.props.account);
}
handleMute = () => {
this.props.onMute(this.props.account);
}
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
}
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
}
handleAction = () => {
this.props.onActionClick(this.props.account);
}
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
if (!account) {
return <div />;
}
if (hidden) {
return (
<Fragment>
{account.get('display_name')}
{account.get('username')}
</Fragment>
);
}
let buttons;
if (onActionClick && actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) {
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<Fragment>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton}
</Fragment>
);
} else if (!account.get('moved') || following) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
}
}
return (
<div className='account'>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Link>
<div className='account__relationship'>
{buttons}
</div>
</div>
</div>
);
}
}
export { default } from './account';

View File

@ -0,0 +1,35 @@
import unicodeMapping from '../emoji/emoji_unicode_mapping_light';
import './autosuggest_emoji.scss';
const assetHost = process.env.CDN_HOST || '';
export default class AutosuggestEmoji extends PureComponent {
static propTypes = {
emoji: PropTypes.object.isRequired,
};
render () {
const { emoji } = this.props;
let url;
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
if (!mapping) return null;
url = `${assetHost}/emoji/${mapping.filename}.svg`;
}
return (
<div className='autosuggest-emoji'>
<img className='emojione' src={url} alt={emoji.native || emoji.colons} />
{emoji.colons}
</div>
);
}
}

View File

@ -1,35 +1 @@
import unicodeMapping from '../../features/emoji/emoji_unicode_mapping_light';
import './index.scss';
const assetHost = process.env.CDN_HOST || '';
export default class AutosuggestEmoji extends PureComponent {
static propTypes = {
emoji: PropTypes.object.isRequired,
};
render () {
const { emoji } = this.props;
let url;
if (emoji.custom) {
url = emoji.imageUrl;
} else {
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
if (!mapping) return null;
url = `${assetHost}/emoji/${mapping.filename}.svg`;
}
return (
<div className='autosuggest-emoji'>
<img className='emojione' src={url} alt={emoji.native || emoji.colons} />
{emoji.colons}
</div>
);
}
}
export { default } from './autosuggest_emoji';

View File

@ -0,0 +1,267 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import AutosuggestAccount from '../../features/compose/components/autosuggest_account';
import AutosuggestEmoji from '../autosuggest_emoji';
import { isRtl } from '../../utils/rtl';
import { textAtCursorMatchesToken } from '../../utils/cursor_token_match';
import './autosuggest_textbox.scss';
export default class AutosuggestTextbox extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
onPaste: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
textarea: PropTypes.bool,
};
static defaultProps = {
autoFocus: true,
searchTokens: ['@', ':', '#'],
textarea: false,
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e);
}
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
if (disabled) {
e.preventDefault();
return;
}
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
if (e.which === 229 || e.isComposing) return;
switch(e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) return;
this.props.onKeyDown(e);
}
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
if (this.props.onBlur) {
this.props.onBlur();
}
}
onFocus = () => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus();
}
}
onPaste = (e) => {
if (this.props.onPaste && e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
e.preventDefault();
}
}
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textbox.focus();
}
componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}
setTextbox = (c) => {
this.textbox = c;
}
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccount id={suggestion} />;
key = suggestion;
}
const classes = classNames('autosuggest-textarea__suggestions__item', {
selected: i === selectedSuggestion,
});
return (
<div
role='button'
tabIndex='0'
key={key}
data-index={i}
className={classes}
onMouseDown={this.onSuggestionClick}
>
{inner}
</div>
);
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, className, id, maxLength, textarea } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
if (isRtl(value)) {
style.direction = 'rtl';
}
if (textarea) {
return [
<div className='autosuggest-textarea__wrapper' key='autosuggest-textarea__wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea
inputRef={this.setTextbox}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
aria-autocomplete='list'
/>
</label>
</div>
{children}
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='autosuggest-textarea__suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>,
];
}
return (
<div className='autosuggest-input'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<input
type='text'
ref={this.setTextbox}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={style}
aria-autocomplete='list'
id={id}
className={className}
maxLength={maxLength}
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>
);
}
}

View File

@ -1,267 +1 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from '../../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from '../autosuggest_emoji';
import { isRtl } from '../../utils/rtl';
import { textAtCursorMatchesToken } from '../../utils/cursor_token_match';
import './index.scss';
export default class AutosuggestTextbox extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
suggestions: ImmutablePropTypes.list,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
onSuggestionSelected: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onKeyUp: PropTypes.func,
onKeyDown: PropTypes.func,
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
searchTokens: PropTypes.arrayOf(PropTypes.string),
maxLength: PropTypes.number,
onPaste: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
textarea: PropTypes.bool,
};
static defaultProps = {
autoFocus: true,
searchTokens: ['@', ':', '#'],
textarea: false,
};
state = {
suggestionsHidden: true,
focused: false,
selectedSuggestion: 0,
lastToken: null,
tokenStart: 0,
};
onChange = (e) => {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart, this.props.searchTokens);
if (token !== null && this.state.lastToken !== token) {
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token);
} else if (token === null) {
this.setState({ lastToken: null });
this.props.onSuggestionsClearRequested();
}
this.props.onChange(e);
}
onKeyDown = (e) => {
const { suggestions, disabled } = this.props;
const { selectedSuggestion, suggestionsHidden } = this.state;
if (disabled) {
e.preventDefault();
return;
}
// Ignore key events during text composition
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
if (e.which === 229 || e.isComposing) return;
switch(e.key) {
case 'Escape':
if (suggestions.size === 0 || suggestionsHidden) {
document.querySelector('.ui').parentElement.focus();
} else {
e.preventDefault();
this.setState({ suggestionsHidden: true });
}
break;
case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
}
break;
case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
}
break;
case 'Enter':
case 'Tab':
// Select suggestion
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault();
e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
}
break;
}
if (e.defaultPrevented || !this.props.onKeyDown) return;
this.props.onKeyDown(e);
}
onBlur = () => {
this.setState({ suggestionsHidden: true, focused: false });
if (this.props.onBlur) {
this.props.onBlur();
}
}
onFocus = () => {
this.setState({ focused: true });
if (this.props.onFocus) {
this.props.onFocus();
}
}
onPaste = (e) => {
if (this.props.onPaste && e.clipboardData && e.clipboardData.files.length === 1) {
this.props.onPaste(e.clipboardData.files);
e.preventDefault();
}
}
onSuggestionClick = (e) => {
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
e.preventDefault();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
this.textbox.focus();
}
componentWillReceiveProps (nextProps) {
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
this.setState({ suggestionsHidden: false });
}
}
setTextbox = (c) => {
this.textbox = c;
}
renderSuggestion = (suggestion, i) => {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (suggestion[0] === '#') {
inner = suggestion;
key = suggestion;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
}
const classes = classNames('autosuggest-textarea__suggestions__item', {
selected: i === selectedSuggestion,
});
return (
<div
role='button'
tabIndex='0'
key={key}
data-index={i}
className={classes}
onMouseDown={this.onSuggestionClick}
>
{inner}
</div>
);
}
render () {
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, className, id, maxLength, textarea } = this.props;
const { suggestionsHidden } = this.state;
const style = { direction: 'ltr' };
if (isRtl(value)) {
style.direction = 'rtl';
}
if (textarea) {
return [
<div className='autosuggest-textarea__wrapper' key='autosuggest-textarea__wrapper'>
<div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea
inputRef={this.setTextbox}
className='autosuggest-textarea__textarea'
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
onPaste={this.onPaste}
style={style}
aria-autocomplete='list'
/>
</label>
</div>
{children}
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='autosuggest-textarea__suggestions-wrapper'>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>,
];
}
return (
<div className='autosuggest-input'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<input
type='text'
ref={this.setTextbox}
disabled={disabled}
placeholder={placeholder}
autoFocus={autoFocus}
value={value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onKeyUp={onKeyUp}
onFocus={this.onFocus}
onBlur={this.onBlur}
style={style}
aria-autocomplete='list'
id={id}
className={className}
maxLength={maxLength}
/>
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
{suggestions.map(this.renderSuggestion)}
</div>
</div>
);
}
}
export { default } from './autosuggest_textbox';

View File

@ -0,0 +1,69 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Map as ImmutableMap } from 'immutable';
import classNames from 'classnames';
import { autoPlayGif } from '../../initial_state';
import './avatar.scss';
export default class Avatar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
size: PropTypes.number,
inline: PropTypes.bool,
animate: PropTypes.bool,
};
static defaultProps = {
account: ImmutableMap(),
animate: autoPlayGif,
inline: false,
};
state = {
hovering: false,
sameImg: this.props.account.get('avatar') === this.props.account.get('avatar_static'),
};
handleMouseEnter = () => {
if (this.props.animate || this.state.sameImg) return;
this.setState({ hovering: true });
}
handleMouseLeave = () => {
if (this.props.animate || this.state.sameImg) return;
this.setState({ hovering: false });
}
render () {
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
// : TODO : remove inline and change all avatars to be sized using css
const style = !size ? {} : {
width: `${size}px`,
height: `${size}px`,
};
const theSrc = account.get((hovering || animate) ? 'avatar' : 'avatar_static');
const className = classNames('account__avatar', {
'account__avatar--inline': inline,
});
return (
<img
className={className}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}
src={theSrc}
alt={account.get('display_name')}
/>
);
}
}

View File

@ -1,69 +1 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { Map as ImmutableMap } from 'immutable';
import classNames from 'classnames';
import { autoPlayGif } from '../../initial_state';
import './index.scss';
export default class Avatar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
size: PropTypes.number,
inline: PropTypes.bool,
animate: PropTypes.bool,
};
static defaultProps = {
account: ImmutableMap(),
animate: autoPlayGif,
inline: false,
};
state = {
hovering: false,
sameImg: this.props.account.get('avatar') === this.props.account.get('avatar_static'),
};
handleMouseEnter = () => {
if (this.props.animate || this.state.sameImg) return;
this.setState({ hovering: true });
}
handleMouseLeave = () => {
if (this.props.animate || this.state.sameImg) return;
this.setState({ hovering: false });
}
render () {
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
// : TODO : remove inline and change all avatars to be sized using css
const style = !size ? {} : {
width: `${size}px`,
height: `${size}px`,
};
const theSrc = account.get((hovering || animate) ? 'avatar' : 'avatar_static');
const className = classNames('account__avatar', {
'account__avatar--inline': inline,
});
return (
<img
className={className}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
style={style}
src={theSrc}
alt={account.get('display_name')}
/>
);
}
}
export { default } from './avatar';

View File

@ -0,0 +1,33 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif } from '../../initial_state';
import './avatar_overlay.scss';
export default class AvatarOverlay extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
render() {
const { account, friend, animate } = this.props;
const baseSrc = account.get(animate ? 'avatar' : 'avatar_static');
const overlaySrc = friend.get(animate ? 'avatar' : 'avatar_static');
return (
<div className='avatar-overlay'>
<img className='avatar-overlay__base' src={baseSrc} />
<img className='avatar-overlay__overlay' src={overlaySrc} />
</div>
);
}
}

View File

@ -1,33 +1 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif } from '../../initial_state';
import './index.scss';
export default class AvatarOverlay extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
};
render() {
const { account, friend, animate } = this.props;
const baseSrc = account.get(animate ? 'avatar' : 'avatar_static');
const overlaySrc = friend.get(animate ? 'avatar' : 'avatar_static');
return (
<div className='avatar-overlay'>
<img className='avatar-overlay__base' src={baseSrc} />
<img className='avatar-overlay__overlay' src={overlaySrc} />
</div>
);
}
}
export { default } from './avatar_overlay';

View File

@ -0,0 +1,25 @@
import './badge.scss';
export default class Badge extends PureComponent {
static propTypes = {
type: PropTypes.oneOf([
'pro',
'donor',
'investor',
]).isRequired,
};
render() {
const { type } = this.props;
if (!type) return null;
return (
<span className={`badge badge--${type}`}>
{type.toUpperCase()}
</span>
);
}
};

View File

@ -1,25 +1 @@
import './index.scss';
export default class Badge extends PureComponent {
static propTypes = {
type: PropTypes.oneOf([
'pro',
'donor',
'investor',
]).isRequired,
};
render() {
const { type } = this.props;
if (!type) return null;
return (
<span className={`badge badge--${type}`}>
{type.toUpperCase()}
</span>
);
}
};
export { default } from './badge';

View File

@ -1,8 +1,9 @@
import { defineMessages, injectIntl } from 'react-intl';
import Column from '../column';
import ColumnHeader from '../column_header';
import IconButton from '../icon_button';
import Column from './column';
import ColumnHeader from './column_header';
import IconButton from '../../../components/icon_button';
import './bundle_column_error.scss';
const messages = defineMessages({
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
@ -10,6 +11,7 @@ const messages = defineMessages({
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
});
export default @injectIntl
class BundleColumnError extends PureComponent {
static propTypes = {
@ -36,5 +38,3 @@ class BundleColumnError extends PureComponent {
}
}
export default injectIntl(BundleColumnError);

View File

@ -0,0 +1,29 @@
.error-column {
color: $dark-text-color;
background: $ui-base-color;
padding: 40px;
cursor: default;
flex: 1 1 auto;
min-height: 160px;
@include flex(center, center, column);
@include text-sizing(15px, 400, 1, center);
@supports(display: grid) {
// hack to fix Chrome <57
contain: strict;
}
&>span {
max-width: 400px;
}
a {
color: $highlight-text-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}

View File

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

View File

@ -1,6 +1,7 @@
import { defineMessages, injectIntl } from 'react-intl';
import IconButton from '../icon_button';
import IconButton from '../../../components/icon_button';
import './bundle_modal_error.scss';
const messages = defineMessages({
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
@ -8,6 +9,7 @@ const messages = defineMessages({
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
});
export default @injectIntl
class BundleModalError extends PureComponent {
static propTypes = {
@ -48,4 +50,3 @@ class BundleModalError extends PureComponent {
}
export default injectIntl(BundleModalError);

View File

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

View File

@ -0,0 +1,49 @@
import classNames from 'classnames';
import './button.scss';
export default class Button extends PureComponent {
static propTypes = {
text: PropTypes.node,
onClick: PropTypes.func,
disabled: PropTypes.bool,
block: PropTypes.bool,
secondary: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node,
};
handleClick = (e) => {
if (!this.props.disabled && this.props.onClick) {
this.props.onClick(e);
}
}
setRef = (c) => {
this.node = c;
}
focus() {
this.node.focus();
}
render () {
const className = classNames('button', this.props.className, {
'button--secondary': this.props.secondary,
'button--block': this.props.block,
});
return (
<button
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
>
{this.props.text || this.props.children}
</button>
);
}
}

View File

@ -1,47 +1 @@
import classNames from 'classnames';
export default class Button extends PureComponent {
static propTypes = {
text: PropTypes.node,
onClick: PropTypes.func,
disabled: PropTypes.bool,
block: PropTypes.bool,
secondary: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.node,
};
handleClick = (e) => {
if (!this.props.disabled && this.props.onClick) {
this.props.onClick(e);
}
}
setRef = (c) => {
this.node = c;
}
focus() {
this.node.focus();
}
render () {
const className = classNames('button', this.props.className, {
'button--secondary': this.props.secondary,
'button--block': this.props.block,
});
return (
<button
className={className}
disabled={this.props.disabled}
onClick={this.handleClick}
ref={this.setRef}
>
{this.props.text || this.props.children}
</button>
);
}
}
export { default } from './button';

View File

@ -1,7 +1,8 @@
import ColumnHeader from './column_header';
import { isMobile } from '../../../utils/is_mobile';
import ColumnBackButton from '../../../components/column_back_button';
import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
import ColumnHeader from '../column_header';
import { isMobile } from '../../utils/is_mobile';
import ColumnBackButton from '../column_back_button';
import './column.scss';
export default class Column extends PureComponent {
@ -11,25 +12,24 @@ export default class Column extends PureComponent {
children: PropTypes.node,
active: PropTypes.bool,
hideHeadingOnMobile: PropTypes.bool,
backBtnSlim: PropTypes.bool,
backBtn: PropTypes.oneOf([
'normal',
'slim',
]),
};
render () {
const { heading, icon, children, active, hideHeadingOnMobile, backBtnSlim } = this.props;
const { heading, icon, children, active, hideHeadingOnMobile, backBtn } = this.props;
const showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth)));
const columnHeaderId = showHeading && heading.replace(/ /g, '-');
const header = showHeading && (
<ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />
);
const backBtn = backBtnSlim ? (<ColumnBackButtonSlim/>) : (<ColumnBackButton/>);
// const header = showHeading && (
// <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />
// );
return (
<div role='region' aria-labelledby={columnHeaderId} className='column'>
{header}
{backBtn}
{ backBtn && <ColumnBackButton slim={backBtn === 'slim'} />}
{children}
</div>
);

View File

@ -1,20 +1 @@
import './index.scss';
export default class Column extends PureComponent {
static propTypes = {
children: PropTypes.node,
label: PropTypes.string,
};
render () {
const { label, children } = this.props;
return (
<div role='region' aria-label={label} className='column'>
{children}
</div>
);
}
}
export { default } from './column';

View File

@ -0,0 +1,40 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from '../icon';
import './column_back_button.scss';
export default class ColumnBackButton extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
slim: PropTypes.bool,
};
handleClick = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/home'); // homehack
} else {
this.context.router.history.goBack();
}
}
render () {
const { slim } = this.props;
const btnClasses = classNames('column-back-button', {
'column-back-button--slim': slim,
});
return (
<button className={btnClasses} onClick={this.handleClick}>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
}
}

View File

@ -1,40 +1 @@
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Icon from '../icon';
import './index.scss';
export default class ColumnBackButton extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
slim: PropTypes.bool,
};
handleClick = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/home'); // homehack
} else {
this.context.router.history.goBack();
}
}
render () {
const { slim } = this.props;
const btnClasses = classNames('column-back-button', {
'column-back-button--slim': slim,
});
return (
<button className={btnClasses} onClick={this.handleClick}>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
}
}
export { default } from './column_back_button';

View File

@ -0,0 +1,125 @@
import { Fragment } from 'react';
import classNames from 'classnames';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from '../icon';
import './column_header.scss';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
});
export default @injectIntl
class ColumnHeader extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
intl: PropTypes.object.isRequired,
title: PropTypes.node,
icon: PropTypes.string,
active: PropTypes.bool,
children: PropTypes.node,
};
state = {
collapsed: true,
};
historyBack = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/home'); // homehack
} else {
this.context.router.history.goBack();
}
}
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({
collapsed: !this.state.collapsed,
});
}
handleBackClick = () => {
this.historyBack();
}
render () {
const { title, icon, active, children, intl: { formatMessage } } = this.props;
const { collapsed } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'column-header__wrapper--active': active,
});
const buttonClassName = classNames('column-header', {
'column-header--active': active,
});
const btnTitle = formatMessage(collapsed ? messages.show : messages.hide);
const hasTitle = icon && title;
const hasChildren = !!children;
if (!hasChildren && !hasTitle) {
return null;
} else if (!hasChildren && hasTitle) {
return (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
<Icon id={icon} fixedWidth className='column-header__icon' />
{title}
</h1>
</div>
);
}
const collapsibleClassName = classNames('column-header__collapsible', {
'column-header__collapsible--collapsed': collapsed,
});
const collapsibleButtonClassName = classNames('column-header__button', {
'column-header__button--active': !collapsed,
});
return (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
{
hasTitle && (
<Fragment>
<Icon id={icon} fixedWidth className='column-header__icon' />
{title}
</Fragment>
)
}
<button
className={collapsibleButtonClassName}
title={btnTitle}
aria-label={btnTitle}
aria-pressed={!collapsed}
onClick={this.handleToggleClick}
>
<Icon id='sliders' />
</button>
</h1>
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null}>
<div className='column-header__collapsible-inner'>
{
!collapsed &&
<div key='extra-content' className='column-header__collapsible__extra'>
{children}
</div>
}
</div>
</div>
</div>
);
}
}

View File

@ -35,7 +35,6 @@
&--active {
&::before {
margin: 0 auto;
pointer-events: none;
z-index: 1;
background: radial-gradient(ellipse, rgba($ui-highlight-color, 0.23) 0%, rgba($ui-highlight-color, 0) 60%);
@ -43,6 +42,7 @@
@include pseudo;
@include size(60%, 28px);
@include abs-position(35px, 0, auto, 0, false);
@include margin-center;
}
}
}

View File

@ -7,7 +7,7 @@ import { createSelector } from 'reselect';
import Icon from '../icon';
import { fetchLists } from '../../actions/lists';
import './index.scss';
import './column_header.scss';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
@ -31,7 +31,9 @@ const mapStateToProps = state => {
};
};
class ColumnHeader extends ImmutablePureComponent {
export default @connect(mapStateToProps)
@injectIntl
class HomeColumnHeader extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@ -166,5 +168,3 @@ class ColumnHeader extends ImmutablePureComponent {
}
}
export default injectIntl(connect(mapStateToProps)(ColumnHeader));

View File

@ -1,125 +1,7 @@
import { Fragment } from 'react';
import classNames from 'classnames';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from '../icon';
import './index.scss';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
});
export default @injectIntl
class ColumnHeader extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
intl: PropTypes.object.isRequired,
title: PropTypes.node,
icon: PropTypes.string,
active: PropTypes.bool,
children: PropTypes.node,
};
state = {
collapsed: true,
};
historyBack = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/home'); // homehack
} else {
this.context.router.history.goBack();
}
}
handleToggleClick = (e) => {
e.stopPropagation();
this.setState({
collapsed: !this.state.collapsed,
});
}
handleBackClick = () => {
this.historyBack();
}
render () {
const { title, icon, active, children, intl: { formatMessage } } = this.props;
const { collapsed } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
'column-header__wrapper--active': active,
});
const buttonClassName = classNames('column-header', {
'column-header--active': active,
});
const btnTitle = formatMessage(collapsed ? messages.show : messages.hide);
const hasTitle = icon && title;
const hasChildren = !!children;
if (!hasChildren && !hasTitle) {
return null;
} else if (!hasChildren && hasTitle) {
return (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
<Icon id={icon} fixedWidth className='column-header__icon' />
{title}
</h1>
</div>
);
}
const collapsibleClassName = classNames('column-header__collapsible', {
'column-header__collapsible--collapsed': collapsed,
});
const collapsibleButtonClassName = classNames('column-header__button', {
'column-header__button--active': !collapsed,
});
return (
<div className={wrapperClassName}>
<h1 className={buttonClassName}>
{
hasTitle && (
<Fragment>
<Icon id={icon} fixedWidth className='column-header__icon' />
{title}
</Fragment>
)
}
<button
className={collapsibleButtonClassName}
title={btnTitle}
aria-label={btnTitle}
aria-pressed={!collapsed}
onClick={this.handleToggleClick}
>
<Icon id='sliders' />
</button>
</h1>
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null}>
<div className='column-header__collapsible-inner'>
{
!collapsed &&
<div key='extra-content' className='column-header__collapsible__extra'>
{children}
</div>
}
</div>
</div>
</div>
);
}
import ColumnHeader from './column_header';
import HomeColumnHeader from './home_column_header';
export {
ColumnHeader,
HomeColumnHeader,
}

View File

@ -0,0 +1,42 @@
import { defineMessages, injectIntl } from 'react-intl';
import Column from '../column';
import './column_indicator.scss';
const messages = defineMessages({
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading...' },
missing: { id: 'missing_indicator.sublabel', defaultMessage: 'This resource could not be found.' },
});
export default @injectIntl
class ColumnIndicator extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
type: PropTypes.oneOf([
'loading',
'missing',
'error',
]),
message: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
};
render() {
const { type, message, intl } = this.props;
const title = type !== 'error' ? intl.formatMessage(messages[type]) : message;
return (
<Column>
<div className={`column-indicator column-indicator--${type}`}>
<div className='column-indicator__figure' />
<span className='column-indicator__title'>{title}</span>
</div>
</Column>
);
}
};

View File

@ -1,41 +1 @@
import { defineMessages, injectIntl } from 'react-intl';
import Column from '../column';
import './index.scss';
const messages = defineMessages({
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading...' },
missing: { id: 'missing_indicator.sublabel', defaultMessage: 'This resource could not be found.' },
});
export default @injectIntl
class ColumnIndicator extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
type: PropTypes.oneOf([
'loading',
'missing',
'error',
]),
message: PropTypes.oneOfType([
PropTypes.string,
PropTypes.object,
]),
};
render() {
const { type, message, intl } = this.props;
const title = type !== 'error' ? intl.formatMessage(messages[type]) : message;
return (
<Column>
<div className={`column-indicator column-indicator--${type}`}>
<div className='column-indicator__figure' />
<span className='column-indicator__title'>{title}</span>
</div>
</Column>
);
}
};
export { default } from './column_indicator';

View File

@ -0,0 +1,22 @@
import { Link } from 'react-router-dom';
import Icon from '../../components/icon';
export default class ColumnLink extends PureComponent {
static propTypes = {
icon: PropTypes.string.isRequired,
text: PropTypes.string.isRequired,
to: PropTypes.string,
};
render() {
const { to, icon, text } = this.props;
return (
<Link to={to} className='column-link'>
<Icon id={icon} fixedWidth className='column-link__icon' />
{text}
</Link>
);
}
};

View File

@ -0,0 +1,39 @@
.column-link {
background: lighten($ui-base-color, 8%);
color: $primary-text-color;
display: block;
font-size: 16px;
padding: 15px;
text-decoration: none;
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 11%);
}
&:focus {
outline: 0;
}
&--transparent {
background: transparent;
color: $ui-secondary-color;
&:hover,
&:focus,
&:active {
background: transparent;
color: $primary-text-color;
}
&.active {
color: $ui-highlight-color;
}
}
&__icon {
display: inline-block;
margin-right: 5px;
}
}

View File

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

View File

@ -0,0 +1,17 @@
import './column_subheading.scss';
export default class ColumnSubheading extends PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
};
render() {
const { text } = this.props;
return (
<div className='column-subheading'>
{text}
</div>
);
}
};

View File

@ -0,0 +1,9 @@
.column-subheading {
background: $ui-base-color;
color: $dark-text-color;
padding: 8px 20px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
cursor: default;
}

View File

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

View File

@ -0,0 +1,42 @@
import './columns_area.scss';
export default class ColumnsArea extends PureComponent {
static propTypes = {
children: PropTypes.node,
layout: PropTypes.object,
};
render () {
const { children } = this.props;
const layout = this.props.layout || {LEFT:null,RIGHT:null};
return (
<div className='page'>
<div className='page__columns'>
<div className='columns-area__panels'>
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
<div className='columns-area__panels__pane__inner'>
{layout.LEFT}
</div>
</div>
<div className='columns-area__panels__main'>
<div className='columns-area columns-area--mobile'>
{children}
</div>
</div>
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
<div className='columns-area__panels__pane__inner'>
{layout.RIGHT}
</div>
</div>
</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,187 @@
.page {
display: flex;
flex-direction: column;
width: 100%;
&__top {
display: flex;
z-index: 105;
@include size(100%, auto);
@media (min-width:895px) {
top: -290px;
position: sticky;
}
}
&__columns {
display: flex;
flex-direction: column;
@include size(100%);
}
}
.columns-area {
display: flex;
flex: 1 1 auto;
flex-direction: row;
justify-content: flex-start;
position: relative;
&__panels {
display: flex;
justify-content: center;
@include size(100%);
&__pane {
height: 100%;
pointer-events: none;
display: flex;
justify-content: flex-end;
padding-top: 15px;
&--start {
justify-content: flex-start;
}
&__inner {
pointer-events: auto;
@include size(265px, 100%);
}
}
&__main {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
max-width: 600px;
padding: 0 20px;
@media screen and (max-width: 375px) {
padding: 0 10px;
}
@media screen and (min-width: 895px) {
margin: 0 20px;
padding: 0;
}
}
}
}
@media screen and (min-width: 631px) {
.columns-area {
padding: 0;
}
.columns-area>div {
.column,
.drawer {
padding-left: 5px;
padding-right: 5px;
}
}
}
.columns-area--mobile {
flex-direction: column;
width: 100%;
@include margin-center;
.column,
.drawer {
padding: 0;
@include size(100%);
}
.search__input {
line-height: 18px;
font-size: 16px;
padding: 15px 30px 15px 15px;
}
.search__icon .fa {
top: 15px;
}
@media screen and (min-width: 360px) {
padding: 15px 0;
}
@media screen and (min-width: 630px) {
.detailed-status {
padding: 15px;
.media-gallery,
.video-player {
margin-top: 15px;
}
}
.account__header__bar {
padding: 5px 10px;
}
.navigation-bar,
.compose-form {
padding: 15px;
}
.compose-form .compose-form__publish .compose-form__publish-button-wrapper {
padding-top: 15px;
}
.account {
padding: 15px 10px;
}
.notification {
&__message {
margin-left: 48px + 15px * 2;
padding-top: 15px;
}
&__favourite-icon-wrapper {
left: -32px;
}
.status {
padding-top: 8px;
}
.account {
padding-top: 8px;
}
.account__avatar-wrapper {
margin-left: 17px;
margin-right: 15px;
}
}
}
}
@media screen and (max-width: 895px) {
.columns-area__panels__pane--left {
display: none;
}
}
@media screen and (max-width: 1180px) {
.columns-area__panels__pane--right {
display: none;
}
}
.columns-area--mobile .column {
// @include gab-container-standards();
}

View File

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

View File

@ -0,0 +1,27 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import VerifiedIcon from '../verified_icon/verified_icon';
import './display_name.scss';
export default class DisplayName extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
};
render () {
const { account } = this.props;
return (
<span className='display-name'>
<bdi>
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
</bdi>
{account.get('is_verified') && <VerifiedIcon />}
<span className='display-name__account'>@{account.get('acct')}</span>
</span>
);
}
}

View File

@ -1,27 +1 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import VerifiedIcon from '../verified_icon';
import './index.scss';
export default class DisplayName extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
};
render () {
const { account } = this.props;
return (
<span className='display-name'>
<bdi>
<strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
</bdi>
{account.get('is_verified') && <VerifiedIcon />}
<span className='display-name__account'>@{account.get('acct')}</span>
</span>
);
}
}
export { default } from './display_name';

View File

@ -0,0 +1,48 @@
import IconButton from '../icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import './domain.scss';
const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});
export default @injectIntl
class Domain extends PureComponent {
static propTypes = {
domain: PropTypes.string,
onUnblockDomain: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleDomainUnblock = () => {
this.props.onUnblockDomain(this.props.domain);
}
render () {
const { domain, intl } = this.props;
return (
<div className='domain'>
<div className='domain__wrapper'>
<span className='domain__name'>
<strong>{domain}</strong>
</span>
<div className='domain__buttons'>
<IconButton
active
icon='unlock'
title={intl.formatMessage(messages.unblockDomain, {
domain,
})}
onClick={this.handleDomainUnblock}
/>
</div>
</div>
</div>
);
}
}

View File

@ -1,48 +1 @@
import IconButton from '../icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import './index.scss';
const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});
export default @injectIntl
class Domain extends PureComponent {
static propTypes = {
domain: PropTypes.string,
onUnblockDomain: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleDomainUnblock = () => {
this.props.onUnblockDomain(this.props.domain);
}
render () {
const { domain, intl } = this.props;
return (
<div className='domain'>
<div className='domain__wrapper'>
<span className='domain__name'>
<strong>{domain}</strong>
</span>
<div className='domain__buttons'>
<IconButton
active
icon='unlock'
title={intl.formatMessage(messages.unblockDomain, {
domain,
})}
onClick={this.handleDomainUnblock}
/>
</div>
</div>
</div>
);
}
}
export { default } from './domain';

View File

@ -0,0 +1,272 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events';
import Overlay from 'react-overlays/lib/Overlay';
import spring from 'react-motion/lib/spring';
import IconButton from '../icon_button';
import Motion from '../../features/ui/util/optional_motion';
import './dropdown_menu.scss';
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
let id = 0;
class DropdownMenu extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
items: PropTypes.array.isRequired,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
style: {},
placement: 'bottom',
};
state = {
mounted: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
this.setState({ mounted: true });
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
setFocusRef = c => {
this.focusedItem = c;
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement);
let element;
switch(e.key) {
case 'ArrowDown':
element = items[index+1];
if (element) element.focus();
break;
case 'ArrowUp':
element = items[index-1];
if (element) element.focus();
break;
case 'Home':
element = items[0];
if (element) element.focus();
break;
case 'End':
element = items[items.length-1];
if (element) element.focus();
break;
}
}
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e);
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.props.onClose();
if (typeof action === 'function') {
e.preventDefault();
action(e);
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
renderItem (option, i) {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#', newTab, isLogout } = option;
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a
href={href}
role='button'
tabIndex='0'
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onKeyDown={this.handleItemKeyDown}
data-index={i}
target={newTab ? '_blank' : null}
data-method={isLogout ? 'delete' : null}
>
{text}
</a>
</li>
);
}
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { mounted } = this.state;
return (
<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 }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
</div>
)}
</Motion>
);
}
}
export default class Dropdown extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
title: 'Menu',
};
state = {
id: id++,
};
handleClick = ({ target, type }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
handleClose = () => {
this.props.onClose(this.state.id);
}
handleKeyDown = e => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.preventDefault();
break;
case 'Escape':
this.handleClose();
break;
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
componentWillUnmount = () => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
}
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const open = this.state.id === openDropdownId;
return (
<div onKeyDown={this.handleKeyDown}>
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</Overlay>
</div>
);
}
}

View File

@ -1,272 +1 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import detectPassiveEvents from 'detect-passive-events';
import Overlay from 'react-overlays/lib/Overlay';
import spring from 'react-motion/lib/spring';
import IconButton from '../icon_button';
import Motion from '../../features/ui/util/optional_motion';
import './index.scss';
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
let id = 0;
class DropdownMenu extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
items: PropTypes.array.isRequired,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
style: {},
placement: 'bottom',
};
state = {
mounted: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
this.setState({ mounted: true });
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
}
setFocusRef = c => {
this.focusedItem = c;
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement);
let element;
switch(e.key) {
case 'ArrowDown':
element = items[index+1];
if (element) element.focus();
break;
case 'ArrowUp':
element = items[index-1];
if (element) element.focus();
break;
case 'Home':
element = items[0];
if (element) element.focus();
break;
case 'End':
element = items[items.length-1];
if (element) element.focus();
break;
}
}
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e);
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.props.onClose();
if (typeof action === 'function') {
e.preventDefault();
action(e);
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
renderItem (option, i) {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { text, href = '#', newTab, isLogout } = option;
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a
href={href}
role='button'
tabIndex='0'
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onKeyDown={this.handleItemKeyDown}
data-index={i}
target={newTab ? '_blank' : null}
data-method={isLogout ? 'delete' : null}
>
{text}
</a>
</li>
);
}
render () {
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
const { mounted } = this.state;
return (
<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 }) => (
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}
</ul>
</div>
)}
</Motion>
);
}
}
export default class Dropdown extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
disabled: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
isModalOpen: PropTypes.bool.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
title: 'Menu',
};
state = {
id: id++,
};
handleClick = ({ target, type }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
handleClose = () => {
this.props.onClose(this.state.id);
}
handleKeyDown = e => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.preventDefault();
break;
case 'Escape':
this.handleClose();
break;
}
}
handleItemClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
this.handleClose();
if (typeof action === 'function') {
e.preventDefault();
action();
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
setTargetRef = c => {
this.target = c;
}
findTarget = () => {
return this.target;
}
componentWillUnmount = () => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
}
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const open = this.state.id === openDropdownId;
return (
<div onKeyDown={this.handleKeyDown}>
<IconButton
icon={icon}
title={title}
active={open}
disabled={disabled}
size={size}
ref={this.setTargetRef}
onClick={this.handleClick}
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</Overlay>
</div>
);
}
}
export { default } from './dropdown_menu';

View File

@ -0,0 +1,40 @@
import { FormattedMessage } from 'react-intl';
import './error_boundary.scss';
export default class ErrorBoundary extends PureComponent {
static propTypes = {
children: PropTypes.node,
};
state = {
hasError: false,
stackTrace: undefined,
componentStack: undefined,
}
componentDidCatch(error, info) {
this.setState({
hasError: true,
stackTrace: error.stack,
componentStack: info && info.componentStack,
});
}
render() {
const { hasError } = this.state;
if (!hasError) return this.props.children;
return (
<div className='error-boundary'>
<div className='error-boundary__container'>
<FormattedMessage id='alert.unexpected.message' defaultMessage='Error' />
<a className='error-boundary__link' href='/home'>Return Home</a>
</div>
</div>
);
}
}

View File

@ -1,6 +1,6 @@
.error-boundary {
&__container {
margin: auto;
@include margin-center(auto);
}
span {
@ -11,8 +11,9 @@
&__link {
display: block;
margin: 15px auto;
text-align: center;
color: $gab-brand-default;
@include margin-center(15px);
}
}

View File

@ -1,40 +1 @@
import { FormattedMessage } from 'react-intl';
import './index.scss';
export default class ErrorBoundary extends PureComponent {
static propTypes = {
children: PropTypes.node,
};
state = {
hasError: false,
stackTrace: undefined,
componentStack: undefined,
}
componentDidCatch(error, info) {
this.setState({
hasError: true,
stackTrace: error.stack,
componentStack: info && info.componentStack,
});
}
render() {
const { hasError } = this.state;
if (!hasError) return this.props.children;
return (
<div className='error-boundary'>
<div className='error-boundary__container'>
<FormattedMessage id='alert.unexpected.message' defaultMessage='Error' />
<a className='error-boundary__link' href='/home'>Return Home</a>
</div>
</div>
);
}
}
export { default } from './error_boundary';

View File

@ -0,0 +1,62 @@
import './extended_video_player.scss';
export default class ExtendedVideoPlayer extends PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
onClick: PropTypes.func,
};
handleLoadedData = () => {
if (this.props.time) {
this.video.currentTime = this.props.time;
}
}
componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData);
}
componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData);
}
setRef = (c) => {
this.video = c;
}
handleClick = e => {
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
}
render () {
const { src, muted, controls, alt } = this.props;
return (
<div className='extended-video-player'>
<video
ref={this.setRef}
src={src}
autoPlay
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
muted={muted}
controls={controls}
loop={!controls}
onClick={this.handleClick}
/>
</div>
);
}
}

View File

@ -1,62 +1 @@
import './index.scss';
export default class ExtendedVideoPlayer extends PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
time: PropTypes.number,
controls: PropTypes.bool.isRequired,
muted: PropTypes.bool.isRequired,
onClick: PropTypes.func,
};
handleLoadedData = () => {
if (this.props.time) {
this.video.currentTime = this.props.time;
}
}
componentDidMount () {
this.video.addEventListener('loadeddata', this.handleLoadedData);
}
componentWillUnmount () {
this.video.removeEventListener('loadeddata', this.handleLoadedData);
}
setRef = (c) => {
this.video = c;
}
handleClick = e => {
e.stopPropagation();
const handler = this.props.onClick;
if (handler) handler();
}
render () {
const { src, muted, controls, alt } = this.props;
return (
<div className='extended-video-player'>
<video
ref={this.setRef}
src={src}
autoPlay
role='button'
tabIndex='0'
aria-label={alt}
title={alt}
muted={muted}
controls={controls}
loop={!controls}
onClick={this.handleClick}
/>
</div>
);
}
}
export { default } from './extended_video_player';

View File

@ -0,0 +1,24 @@
import './floating_action_button.scss';
export default class FloatingActionButton extends Component {
static propTypes = {
onClick: PropTypes.func.isRequired,
message: PropTypes.string.isRequired,
};
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.message !== this.props.message) {
return true;
}
return false;
}
render() {
const { onClick, message } = this.props;
return (
<button onClick={onClick} className='floating-action-button' aria-label={message} />
)
}
}

View File

@ -0,0 +1,21 @@
.floating-action-button {
display: none;
position: fixed;
z-index: 1000;
border: none;
background-color: transparent;
@include size(61px, 52px);
@include abs-position(auto, 14px, 14px, auto, false);
@include background-image('/assets/images/sprite-main-navigation.png', 161px 152px, -100px 0);
@media screen and (max-width: $nav-breakpoint-3) {
display: flex;
}
&:hover,
&:focus,
&:active {
background-position: -100px -100px;
}
}

View File

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

View File

@ -0,0 +1,23 @@
import classNames from 'classnames';
export default class Icon extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
className: PropTypes.string,
fixedWidth: PropTypes.bool,
};
render () {
const { id, className, fixedWidth, ...other } = this.props;
const classes = classNames('fa', `fa-${id}`, className, {
'fa-fw': fixedWidth,
});
return (
<i role='img' alt={id} className={classes} {...other} />
);
}
}

View File

@ -1,23 +1 @@
import classNames from 'classnames';
export default class Icon extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
className: PropTypes.string,
fixedWidth: PropTypes.bool,
};
render () {
const { id, className, fixedWidth, ...other } = this.props;
const classes = classNames('fa', `fa-${id}`, className, {
'fa-fw': fixedWidth,
});
return (
<i role='img' alt={id} className={classes} {...other} />
);
}
}
export { default } from './icon';

View File

@ -0,0 +1,114 @@
import classNames from 'classnames';
import spring from 'react-motion/lib/spring';
import Motion from '../../features/ui/util/optional_motion';
import Icon from '../icon';
import './icon_button.scss';
export default class IconButton extends PureComponent {
static propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
};
static defaultProps = {
size: 18,
active: false,
disabled: false,
animate: false,
overlay: false,
tabIndex: '0',
};
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick(e);
}
}
render () {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
};
const {
active,
animate,
className,
disabled,
expanded,
icon,
inverted,
overlay,
pressed,
tabIndex,
title,
} = this.props;
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
overlayed: overlay,
});
// Perf optimization: avoid unnecessary <Motion> components unless we actually need to animate.
if (!animate) {
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) => (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button>
)}
</Motion>
);
}
}

View File

@ -1,114 +1 @@
import classNames from 'classnames';
import spring from 'react-motion/lib/spring';
import Motion from '../../features/ui/util/optional_motion';
import Icon from '../icon';
import './index.scss';
export default class IconButton extends PureComponent {
static propTypes = {
className: PropTypes.string,
title: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
onClick: PropTypes.func,
size: PropTypes.number,
active: PropTypes.bool,
pressed: PropTypes.bool,
expanded: PropTypes.bool,
style: PropTypes.object,
disabled: PropTypes.bool,
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
};
static defaultProps = {
size: 18,
active: false,
disabled: false,
animate: false,
overlay: false,
tabIndex: '0',
};
handleClick = (e) => {
e.preventDefault();
if (!this.props.disabled) {
this.props.onClick(e);
}
}
render () {
const style = {
fontSize: `${this.props.size}px`,
width: `${this.props.size * 1.28571429}px`,
height: `${this.props.size * 1.28571429}px`,
lineHeight: `${this.props.size}px`,
...this.props.style,
};
const {
active,
animate,
className,
disabled,
expanded,
icon,
inverted,
overlay,
pressed,
tabIndex,
title,
} = this.props;
const classes = classNames(className, 'icon-button', {
active,
disabled,
inverted,
overlayed: overlay,
});
// Perf optimization: avoid unnecessary <Motion> components unless we actually need to animate.
if (!animate) {
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
return (
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) => (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
disabled={disabled}
>
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button>
)}
</Motion>
);
}
}
export { default } from './icon_button';

View File

@ -1,6 +1,8 @@
import classNames from 'classnames';
import { LoadingBar } from 'react-redux-loading-bar';
import ZoomableImage from './zoomable_image';
import ZoomableImage from '../zoomable_image';
import './image_loader.scss';
export default class ImageLoader extends PureComponent {
@ -32,6 +34,7 @@ export default class ImageLoader extends PureComponent {
if (!this.canvas) {
return null;
}
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
return this._canvasContext;
}

View File

@ -0,0 +1,23 @@
.image-loader {
position: relative;
@include flex(center, center, column);
@include size(100%);
&__preview-canvas {
object-fit: contain;
@include max-size($media-modal-media-max-width, $media-modal-media-max-height);
@include background-image('/assets/images/void.png', contain, center, repeat);
}
&--amorphous & {
&__preview-canvas {
display: none;
}
}
.loading-bar {
position: relative;
}
}

View File

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

View File

@ -1,136 +1 @@
import { is } from 'immutable';
import scheduleIdleTask from '../../utils/schedule_idle_task';
import getRectFromEntry from '../../utils/get_rect_from_entry';
import './index.scss';
// Diff these props in the "rendered" state
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
export default class IntersectionObserverArticle extends Component {
static propTypes = {
intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
saveHeightKey: PropTypes.string,
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node,
};
state = {
isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
shouldComponentUpdate (nextProps, nextState) {
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
// If we're going from rendered to unrendered (or vice versa) then update
if (!!isUnrendered !== !!willBeUnrendered) {
return true;
}
// Otherwise, diff based on props
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
}
componentDidMount () {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.observe(
id,
this.node,
this.handleIntersection
);
this.componentMounted = true;
}
componentWillUnmount () {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.unobserve(id, this.node);
this.componentMounted = false;
}
handleIntersection = (entry) => {
this.entry = entry;
scheduleIdleTask(this.calculateHeight);
this.setState(this.updateStateAfterIntersection);
}
updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: this.entry.isIntersecting,
isHidden: false,
};
}
calculateHeight = () => {
const { onHeightChange, saveHeightKey, id } = this.props;
// Save the height of the fully-rendered element (this is expensive
// on Chrome, where we need to fall back to getBoundingClientRect)
this.height = getRectFromEntry(this.entry).height;
if (onHeightChange && saveHeightKey) {
onHeightChange(saveHeightKey, id, this.height);
}
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) return;
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
render () {
const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && (isHidden || cachedHeight)) {
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: true })}
</article>
);
}
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: false })}
</article>
);
}
}
export { default } from './intersection_observer_article';

View File

@ -0,0 +1,136 @@
import { is } from 'immutable';
import scheduleIdleTask from '../../utils/schedule_idle_task';
import getRectFromEntry from '../../utils/get_rect_from_entry';
import './intersection_observer_article.scss';
// Diff these props in the "rendered" state
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
export default class IntersectionObserverArticle extends Component {
static propTypes = {
intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
saveHeightKey: PropTypes.string,
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node,
};
state = {
isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
shouldComponentUpdate (nextProps, nextState) {
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
// If we're going from rendered to unrendered (or vice versa) then update
if (!!isUnrendered !== !!willBeUnrendered) {
return true;
}
// Otherwise, diff based on props
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
}
componentDidMount () {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.observe(
id,
this.node,
this.handleIntersection
);
this.componentMounted = true;
}
componentWillUnmount () {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.unobserve(id, this.node);
this.componentMounted = false;
}
handleIntersection = (entry) => {
this.entry = entry;
scheduleIdleTask(this.calculateHeight);
this.setState(this.updateStateAfterIntersection);
}
updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: this.entry.isIntersecting,
isHidden: false,
};
}
calculateHeight = () => {
const { onHeightChange, saveHeightKey, id } = this.props;
// Save the height of the fully-rendered element (this is expensive
// on Chrome, where we need to fall back to getBoundingClientRect)
this.height = getRectFromEntry(this.entry).height;
if (onHeightChange && saveHeightKey) {
onHeightChange(saveHeightKey, id, this.height);
}
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) return;
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
render () {
const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && (isHidden || cachedHeight)) {
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: true })}
</article>
);
}
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: false })}
</article>
);
}
}

View File

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

View File

@ -0,0 +1,64 @@
import { defineMessages, injectIntl } from 'react-intl';
import { invitesEnabled, version, repository, source_url, me } from '../../initial_state';
import { openModal } from '../../actions/modal';
import './link_footer.scss';
const messages = defineMessages({
invite: { id:'getting_started.invite', defaultMessage: 'Invite people' },
hotkeys: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' },
security: { id: 'getting_started.security', defaultMessage: 'Security' },
about: { id: 'navigation_bar.info', defaultMessage: 'About' },
developers: { id: 'getting_started.developers', defaultMessage: 'Developers' },
terms: { id: 'getting_started.terms', defaultMessage: 'Terms of Service' },
dmca: { id: 'getting_started.dmca', defaultMessage: 'DMCA' },
terms: { id: 'getting_started.terms_of_sale', defaultMessage: 'Terms of Sale' },
privacy: { id: 'getting_started.privacy', defaultMessage: 'Privacy Policy' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
notice: { id: 'getting_started.open_source_notice', defaultMessage: 'Gab Social is open source software. You can contribute or report issues on our self-hosted GitLab at {gitlab}.' },
});
const mapDispatchToProps = (dispatch) => ({
onOpenHotkeys() {
dispatch(openModal('HOTKEYS'));
},
});
export default @connect(null, mapDispatchToProps)
@injectIntl
class LinkFooter extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
onOpenHotkeys: PropTypes.func.isRequired,
};
render() {
const { onOpenHotkeys, intl } = this.props;
return (
<div className='link-footer'>
<ul>
{(invitesEnabled && me) && <li><a href='/invites'>{intl.formatMessage(messages.invite)}</a> · </li>}
{me && <li><a href='#' onClick={onOpenHotkeys}>{intl.formatMessage(messages.hotkeys)}</a> · </li>}
{me && <li><a href='/auth/edit'>{intl.formatMessage(messages.security)}</a> · </li>}
<li><a href='/about'>{intl.formatMessage(messages.about)}</a> · </li>
<li><a href='/settings/applications'>{intl.formatMessage(messages.developers)}</a> · </li>
<li><a href='/about/tos'>{intl.formatMessage(messages.terms)}</a> · </li>
<li><a href='/about/dmca'>{intl.formatMessage(messages.dmca)}</a> · </li>
<li><a href='/about/sales'>{intl.formatMessage(messages.terms)}</a> · </li>
<li><a href='/about/privacy'>{intl.formatMessage(messages.privacy)}</a></li>
{me && <li> · <a href='/auth/sign_out' data-method='delete'>{intl.formatMessage(messages.logout)}</a></li>}
</ul>
<p>
{intl.formatMessage(messages.invite, {
gitlab: <span><a href={source_url} rel='noopener' target='_blank'>{repository}</a> (v{version})</span>
})}
</p>
<p>© 2019 Gab AI Inc.</p>
</div>
);
}
};

Some files were not shown because too many files have changed in this diff Show More