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:
parent
5505f60119
commit
280dc51d85
@ -6,7 +6,7 @@ import {
|
|||||||
importFetchedAccounts,
|
importFetchedAccounts,
|
||||||
importErrorWhileFetchingAccountByUsername,
|
importErrorWhileFetchingAccountByUsername,
|
||||||
} from './importer';
|
} 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_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
||||||
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { CancelToken, isCancel } from 'axios';
|
import { CancelToken, isCancel } from 'axios';
|
||||||
import { throttle } from 'lodash';
|
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 { tagHistory } from '../settings';
|
||||||
import { useEmoji } from './emojis';
|
import { useEmoji } from './emojis';
|
||||||
import resizeImage from '../utils/resize_image';
|
import resizeImage from '../utils/resize_image';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from '../../features/emoji/emoji';
|
import emojify from '../../components/emoji/emoji';
|
||||||
import { unescapeHTML } from '../../utils/html';
|
import { unescapeHTML } from '../../utils/html';
|
||||||
import { expandSpoilers } from '../../initial_state';
|
import { expandSpoilers } from '../../initial_state';
|
||||||
|
|
||||||
|
@ -22,10 +22,6 @@ export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST';
|
|||||||
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS';
|
||||||
export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL';
|
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_REQUEST = 'PIN_REQUEST';
|
||||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||||
export const PIN_FAIL = 'PIN_FAIL';
|
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) {
|
export function pin(status) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
@ -2,7 +2,7 @@ import api, { getLinks } from '../api';
|
|||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts } from './importer';
|
import { importFetchedAccounts } from './importer';
|
||||||
import { openModal } from './modal';
|
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_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { importFetchedAccounts } from './importer';
|
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_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
|
||||||
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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
|
<button
|
||||||
className="button button-secondary"
|
className="button button--secondary"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
|
@ -65,7 +65,7 @@ describe('<Button />', () => {
|
|||||||
expect(tree).toMatchSnapshot();
|
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 component = renderer.create(<Button secondary />);
|
||||||
const tree = component.toJSON();
|
const tree = component.toJSON();
|
||||||
|
|
||||||
|
128
app/javascript/gabsocial/components/account/account.js
Normal file
128
app/javascript/gabsocial/components/account/account.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,128 +1 @@
|
|||||||
import { Fragment } from 'react';
|
export { default } from './account';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,35 +1 @@
|
|||||||
import unicodeMapping from '../../features/emoji/emoji_unicode_mapping_light';
|
export { default } from './autosuggest_emoji';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,267 +1 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
export { default } from './autosuggest_textbox';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
69
app/javascript/gabsocial/components/avatar/avatar.js
Normal file
69
app/javascript/gabsocial/components/avatar/avatar.js
Normal 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')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,69 +1 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
export { default } from './avatar';
|
||||||
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')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,33 +1 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
export { default } from './avatar_overlay';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
25
app/javascript/gabsocial/components/badge/badge.js
Normal file
25
app/javascript/gabsocial/components/badge/badge.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -1,25 +1 @@
|
|||||||
import './index.scss';
|
export { default } from './badge';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
@ -1,8 +1,9 @@
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
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 './bundle_column_error.scss';
|
||||||
import ColumnHeader from './column_header';
|
|
||||||
import IconButton from '../../../components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
|
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' },
|
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
class BundleColumnError extends PureComponent {
|
class BundleColumnError extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -36,5 +38,3 @@ class BundleColumnError extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(BundleColumnError);
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './bundle_column_error';
|
@ -1,6 +1,7 @@
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
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({
|
const messages = defineMessages({
|
||||||
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this component.' },
|
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' },
|
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
class BundleModalError extends PureComponent {
|
class BundleModalError extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -48,4 +50,3 @@ class BundleModalError extends PureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(BundleModalError);
|
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './bundle_modal_error';
|
49
app/javascript/gabsocial/components/button/button.js
Normal file
49
app/javascript/gabsocial/components/button/button.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,47 +1 @@
|
|||||||
import classNames from 'classnames';
|
export { default } from './button';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,7 +1,8 @@
|
|||||||
import ColumnHeader from './column_header';
|
import ColumnHeader from '../column_header';
|
||||||
import { isMobile } from '../../../utils/is_mobile';
|
import { isMobile } from '../../utils/is_mobile';
|
||||||
import ColumnBackButton from '../../../components/column_back_button';
|
import ColumnBackButton from '../column_back_button';
|
||||||
import ColumnBackButtonSlim from '../../../components/column_back_button_slim';
|
|
||||||
|
import './column.scss';
|
||||||
|
|
||||||
export default class Column extends PureComponent {
|
export default class Column extends PureComponent {
|
||||||
|
|
||||||
@ -11,25 +12,24 @@ export default class Column extends PureComponent {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
hideHeadingOnMobile: PropTypes.bool,
|
hideHeadingOnMobile: PropTypes.bool,
|
||||||
backBtnSlim: PropTypes.bool,
|
backBtn: PropTypes.oneOf([
|
||||||
|
'normal',
|
||||||
|
'slim',
|
||||||
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
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 showHeading = heading && (!hideHeadingOnMobile || (hideHeadingOnMobile && !isMobile(window.innerWidth)));
|
||||||
|
|
||||||
const columnHeaderId = showHeading && heading.replace(/ /g, '-');
|
const columnHeaderId = showHeading && heading.replace(/ /g, '-');
|
||||||
const header = showHeading && (
|
// const header = showHeading && (
|
||||||
<ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />
|
// <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />
|
||||||
);
|
// );
|
||||||
|
|
||||||
const backBtn = backBtnSlim ? (<ColumnBackButtonSlim/>) : (<ColumnBackButton/>);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role='region' aria-labelledby={columnHeaderId} className='column'>
|
<div role='region' aria-labelledby={columnHeaderId} className='column'>
|
||||||
{header}
|
{ backBtn && <ColumnBackButton slim={backBtn === 'slim'} />}
|
||||||
{backBtn}
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -1,20 +1 @@
|
|||||||
import './index.scss';
|
export { default } from './column';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,40 +1 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
export { default } from './column_back_button';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -35,7 +35,6 @@
|
|||||||
|
|
||||||
&--active {
|
&--active {
|
||||||
&::before {
|
&::before {
|
||||||
margin: 0 auto;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: radial-gradient(ellipse, rgba($ui-highlight-color, 0.23) 0%, rgba($ui-highlight-color, 0) 60%);
|
background: radial-gradient(ellipse, rgba($ui-highlight-color, 0.23) 0%, rgba($ui-highlight-color, 0) 60%);
|
||||||
@ -43,6 +42,7 @@
|
|||||||
@include pseudo;
|
@include pseudo;
|
||||||
@include size(60%, 28px);
|
@include size(60%, 28px);
|
||||||
@include abs-position(35px, 0, auto, 0, false);
|
@include abs-position(35px, 0, auto, 0, false);
|
||||||
|
@include margin-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -7,7 +7,7 @@ import { createSelector } from 'reselect';
|
|||||||
import Icon from '../icon';
|
import Icon from '../icon';
|
||||||
import { fetchLists } from '../../actions/lists';
|
import { fetchLists } from '../../actions/lists';
|
||||||
|
|
||||||
import './index.scss';
|
import './column_header.scss';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
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 = {
|
static contextTypes = {
|
||||||
router: PropTypes.object,
|
router: PropTypes.object,
|
||||||
@ -166,5 +168,3 @@ class ColumnHeader extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps)(ColumnHeader));
|
|
@ -1,125 +1,7 @@
|
|||||||
import { Fragment } from 'react';
|
import ColumnHeader from './column_header';
|
||||||
import classNames from 'classnames';
|
import HomeColumnHeader from './home_column_header';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
export {
|
||||||
|
ColumnHeader,
|
||||||
|
HomeColumnHeader,
|
||||||
}
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -1,41 +1 @@
|
|||||||
import { defineMessages, injectIntl } from 'react-intl';
|
export { default } from './column_indicator';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
1
app/javascript/gabsocial/components/column_link/index.js
Normal file
1
app/javascript/gabsocial/components/column_link/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './column_link';
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
@ -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;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './column_subheading';
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './columns_area';
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,27 +1 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
export { default } from './display_name';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
48
app/javascript/gabsocial/components/domain/domain.js
Normal file
48
app/javascript/gabsocial/components/domain/domain.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,48 +1 @@
|
|||||||
import IconButton from '../icon_button';
|
export { default } from './domain';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,272 +1 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
export { default } from './dropdown_menu';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
.error-boundary {
|
.error-boundary {
|
||||||
&__container {
|
&__container {
|
||||||
margin: auto;
|
@include margin-center(auto);
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@ -11,8 +11,9 @@
|
|||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 15px auto;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $gab-brand-default;
|
color: $gab-brand-default;
|
||||||
|
|
||||||
|
@include margin-center(15px);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,40 +1 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
export { default } from './error_boundary';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,62 +1 @@
|
|||||||
import './index.scss';
|
export { default } from './extended_video_player';
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './floating_action_button';
|
23
app/javascript/gabsocial/components/icon/icon.js
Normal file
23
app/javascript/gabsocial/components/icon/icon.js
Normal 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} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,23 +1 @@
|
|||||||
import classNames from 'classnames';
|
export { default } from './icon';
|
||||||
|
|
||||||
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} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
114
app/javascript/gabsocial/components/icon_button/icon_button.js
Normal file
114
app/javascript/gabsocial/components/icon_button/icon_button.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,114 +1 @@
|
|||||||
import classNames from 'classnames';
|
export { default } from './icon_button';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,8 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { LoadingBar } from 'react-redux-loading-bar';
|
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 {
|
export default class ImageLoader extends PureComponent {
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ export default class ImageLoader extends PureComponent {
|
|||||||
if (!this.canvas) {
|
if (!this.canvas) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
|
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
|
||||||
return this._canvasContext;
|
return this._canvasContext;
|
||||||
}
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from './image_loader';
|
@ -1,136 +1 @@
|
|||||||
import { is } from 'immutable';
|
export { default } from './intersection_observer_article';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
1
app/javascript/gabsocial/components/link_footer/index.js
Normal file
1
app/javascript/gabsocial/components/link_footer/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './link_footer';
|
@ -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
Loading…
x
Reference in New Issue
Block a user