kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'origin/develop' into translate_public_header
commit
6b4cdacd67
|
@ -34,20 +34,20 @@ export function fetchChats() {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchChatMessages(chatId) {
|
||||
export function fetchChatMessages(chatId, maxId = null) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId });
|
||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`).then(({ data }) => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, chatMessages: data });
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_REQUEST, chatId, maxId });
|
||||
return api(getState).get(`/api/v1/pleroma/chats/${chatId}/messages`, { params: { max_id: maxId } }).then(({ data }) => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_SUCCESS, chatId, maxId, chatMessages: data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, error });
|
||||
dispatch({ type: CHAT_MESSAGES_FETCH_FAIL, chatId, maxId, error });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function sendChatMessage(chatId, params) {
|
||||
return (dispatch, getState) => {
|
||||
const uuid = uuidv4();
|
||||
const uuid = `末_${Date.now()}_${uuidv4()}`;
|
||||
const me = getState().get('me');
|
||||
dispatch({ type: CHAT_MESSAGE_SEND_REQUEST, chatId, params, uuid, me });
|
||||
return api(getState).post(`/api/v1/pleroma/chats/${chatId}/messages`, params).then(({ data }) => {
|
||||
|
|
|
@ -46,6 +46,8 @@ export function importFetchedAccounts(accounts) {
|
|||
const normalAccounts = [];
|
||||
|
||||
function processAccount(account) {
|
||||
if (!account.id) return;
|
||||
|
||||
pushUnique(normalAccounts, normalizeAccount(account));
|
||||
|
||||
if (account.moved) {
|
||||
|
@ -69,6 +71,8 @@ export function importFetchedStatuses(statuses) {
|
|||
const polls = [];
|
||||
|
||||
function processStatus(status) {
|
||||
if (!status.account.id) return;
|
||||
|
||||
const normalOldStatus = getState().getIn(['statuses', status.id]);
|
||||
const expandSpoilers = getSettings(getState()).get('expandSpoilers');
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
export const PROFILE_HOVER_CARD_OPEN = 'PROFILE_HOVER_CARD_OPEN';
|
||||
export const PROFILE_HOVER_CARD_UPDATE = 'PROFILE_HOVER_CARD_UPDATE';
|
||||
export const PROFILE_HOVER_CARD_CLOSE = 'PROFILE_HOVER_CARD_CLOSE';
|
||||
|
||||
export function openProfileHoverCard(ref, accountId) {
|
||||
return {
|
||||
type: PROFILE_HOVER_CARD_OPEN,
|
||||
ref,
|
||||
accountId,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateProfileHoverCard() {
|
||||
return {
|
||||
type: PROFILE_HOVER_CARD_UPDATE,
|
||||
};
|
||||
}
|
||||
|
||||
export function closeProfileHoverCard(force = false) {
|
||||
return {
|
||||
type: PROFILE_HOVER_CARD_CLOSE,
|
||||
force,
|
||||
};
|
||||
}
|
|
@ -7,7 +7,7 @@ export const SOAPBOX_CONFIG_REQUEST_FAIL = 'SOAPBOX_CONFIG_REQUEST_FAIL';
|
|||
export const defaultConfig = ImmutableMap({
|
||||
logo: '',
|
||||
banner: '',
|
||||
brandColor: '#0482d8', // Azure
|
||||
brandColor: '', // Empty
|
||||
customCss: ImmutableList(),
|
||||
promoPanel: ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
|
@ -40,8 +40,9 @@ export function fetchSoapboxConfig() {
|
|||
|
||||
export function fetchSoapboxJson() {
|
||||
return (dispatch, getState) => {
|
||||
api(getState).get('/instance/soapbox.json').then(response => {
|
||||
dispatch(importSoapboxConfig(response.data));
|
||||
api(getState).get('/instance/soapbox.json').then(({ data }) => {
|
||||
if (!isObject(data)) throw 'soapbox.json failed';
|
||||
dispatch(importSoapboxConfig(data));
|
||||
}).catch(error => {
|
||||
dispatch(soapboxConfigFail(error));
|
||||
});
|
||||
|
@ -49,6 +50,9 @@ export function fetchSoapboxJson() {
|
|||
}
|
||||
|
||||
export function importSoapboxConfig(soapboxConfig) {
|
||||
if (!soapboxConfig.brandColor) {
|
||||
soapboxConfig.brandColor = '#0482d8';
|
||||
};
|
||||
return {
|
||||
type: SOAPBOX_CONFIG_REQUEST_SUCCESS,
|
||||
soapboxConfig,
|
||||
|
@ -56,12 +60,14 @@ export function importSoapboxConfig(soapboxConfig) {
|
|||
}
|
||||
|
||||
export function soapboxConfigFail(error) {
|
||||
if (!error.response) {
|
||||
console.error('Unable to obtain soapbox configuration: ' + error);
|
||||
}
|
||||
return {
|
||||
type: SOAPBOX_CONFIG_REQUEST_FAIL,
|
||||
error,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/46663081
|
||||
function isObject(o) {
|
||||
return o instanceof Object && o.constructor === Object;
|
||||
}
|
||||
|
|
|
@ -4,16 +4,23 @@ exports[`<DisplayName /> renders display name + account name 1`] = `
|
|||
<span
|
||||
className="display-name"
|
||||
>
|
||||
<bdi>
|
||||
<strong
|
||||
className="display-name__html"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Foo</p>",
|
||||
<span
|
||||
className="hover-ref-wrapper"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<bdi>
|
||||
<strong
|
||||
className="display-name__html"
|
||||
dangerouslySetInnerHTML={
|
||||
Object {
|
||||
"__html": "<p>Foo</p>",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</bdi>
|
||||
/>
|
||||
</bdi>
|
||||
</span>
|
||||
<span
|
||||
className="display-name__account"
|
||||
>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { fromJS } from 'immutable';
|
||||
import DisplayName from '../display_name';
|
||||
import { createComponent } from 'soapbox/test_helpers';
|
||||
|
||||
describe('<DisplayName />', () => {
|
||||
it('renders display name + account name', () => {
|
||||
|
@ -10,7 +10,7 @@ describe('<DisplayName />', () => {
|
|||
acct: 'bar@baz',
|
||||
display_name_html: '<p>Foo</p>',
|
||||
});
|
||||
const component = renderer.create(<DisplayName account={account} />);
|
||||
const component = createComponent(<DisplayName account={account} />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import VerificationBadge from './verification_badge';
|
||||
import { acctFull } from '../utils/accounts';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
|
||||
export default class DisplayName extends React.PureComponent {
|
||||
|
||||
|
@ -42,7 +43,9 @@ export default class DisplayName extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<span className='display-name'>
|
||||
{displayName}
|
||||
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||
{displayName}
|
||||
</HoverRefWrapper>
|
||||
{suffix}
|
||||
{children}
|
||||
</span>
|
||||
|
|
|
@ -3,9 +3,16 @@ import { connect } from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from'react-helmet';
|
||||
|
||||
const getNotifTotals = state => {
|
||||
const normNotif = state.getIn(['notifications', 'unread']);
|
||||
const chatNotif = state.get('chats').reduce((acc, curr) => acc + curr.get('unread'), 0);
|
||||
const notifTotals = normNotif + chatNotif;
|
||||
return notifTotals;
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
siteTitle: state.getIn(['instance', 'title']),
|
||||
unreadCount: state.getIn(['notifications', 'unread']),
|
||||
unreadCount: getNotifTotals(state),
|
||||
});
|
||||
|
||||
class SoapboxHelmet extends React.Component {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
openProfileHoverCard,
|
||||
closeProfileHoverCard,
|
||||
} from 'soapbox/actions/profile_hover_card';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
import { isMobile } from 'soapbox/is_mobile';
|
||||
|
||||
const showProfileHoverCard = debounce((dispatch, ref, accountId) => {
|
||||
dispatch(openProfileHoverCard(ref, accountId));
|
||||
}, 1200);
|
||||
|
||||
const handleMouseEnter = (dispatch, ref, accountId) => {
|
||||
return e => {
|
||||
if (!isMobile(window.innerWidth))
|
||||
showProfileHoverCard(dispatch, ref, accountId);
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseLeave = (dispatch) => {
|
||||
return e => {
|
||||
showProfileHoverCard.cancel();
|
||||
setTimeout(() => dispatch(closeProfileHoverCard()), 300);
|
||||
};
|
||||
};
|
||||
|
||||
const handleClick = (dispatch) => {
|
||||
return e => {
|
||||
showProfileHoverCard.cancel();
|
||||
dispatch(closeProfileHoverCard(true));
|
||||
};
|
||||
};
|
||||
|
||||
export const HoverRefWrapper = ({ accountId, children, inline }) => {
|
||||
const dispatch = useDispatch();
|
||||
const ref = useRef();
|
||||
const Elem = inline ? 'span' : 'div';
|
||||
|
||||
return (
|
||||
<Elem
|
||||
ref={ref}
|
||||
className='hover-ref-wrapper'
|
||||
onMouseEnter={handleMouseEnter(dispatch, ref, accountId)}
|
||||
onMouseLeave={handleMouseLeave(dispatch)}
|
||||
onClick={handleClick(dispatch)}
|
||||
>
|
||||
{children}
|
||||
</Elem>
|
||||
);
|
||||
};
|
||||
|
||||
HoverRefWrapper.propTypes = {
|
||||
accountId: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
inline: PropTypes.bool,
|
||||
};
|
||||
|
||||
HoverRefWrapper.defaultProps = {
|
||||
inline: false,
|
||||
};
|
||||
|
||||
export default HoverRefWrapper;
|
|
@ -6,6 +6,7 @@ import { is } from 'immutable';
|
|||
import IconButton from './icon_button';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { isIOS } from '../is_mobile';
|
||||
import { truncateFilename } from 'soapbox/utils/media';
|
||||
import classNames from 'classnames';
|
||||
import { decode } from 'blurhash';
|
||||
import { isPanoramic, isPortrait, isNonConformingRatio, minimumAspectRatio, maximumAspectRatio } from '../utils/media_aspect_ratio';
|
||||
|
@ -14,6 +15,8 @@ import { getSettings } from 'soapbox/actions/settings';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
|
||||
const MAX_FILENAME_LENGTH = 45;
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||
});
|
||||
|
@ -143,10 +146,13 @@ class Item extends React.PureComponent {
|
|||
let thumbnail = '';
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
const filename = truncateFilename(attachment.get('remote_url'), MAX_FILENAME_LENGTH);
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
<span className='media-gallery__item__icons'><Icon id='file' /></span>
|
||||
<span className='media-gallery__filename__label'>{filename}</span>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
@ -214,7 +220,7 @@ class Item extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<div className={classNames('media-gallery__item', `media-gallery__item--${attachment.get('type')}`, { standalone })} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
||||
{visible && thumbnail}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import UserPanel from 'soapbox/features/ui/components/user_panel';
|
||||
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||
import { isAdmin, isModerator } from 'soapbox/utils/accounts';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import classNames from 'classnames';
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
import { usePopper } from 'react-popper';
|
||||
import {
|
||||
closeProfileHoverCard,
|
||||
updateProfileHoverCard,
|
||||
} from 'soapbox/actions/profile_hover_card';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const getBadges = (account) => {
|
||||
let badges = [];
|
||||
if (isAdmin(account)) badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
||||
if (isModerator(account)) badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
||||
if (account.getIn(['patron', 'is_patron'])) badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
||||
return badges;
|
||||
};
|
||||
|
||||
const handleMouseEnter = (dispatch) => {
|
||||
return e => {
|
||||
dispatch(updateProfileHoverCard());
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseLeave = (dispatch) => {
|
||||
return e => {
|
||||
dispatch(closeProfileHoverCard(true));
|
||||
};
|
||||
};
|
||||
|
||||
export const ProfileHoverCard = ({ visible }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [popperElement, setPopperElement] = useState(null);
|
||||
|
||||
const accountId = useSelector(state => state.getIn(['profile_hover_card', 'accountId']));
|
||||
const account = useSelector(state => accountId && getAccount(state, accountId));
|
||||
const targetRef = useSelector(state => state.getIn(['profile_hover_card', 'ref', 'current']));
|
||||
const badges = account ? getBadges(account) : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) dispatch(fetchRelationships([accountId]));
|
||||
}, [dispatch, accountId]);
|
||||
|
||||
const { styles, attributes } = usePopper(targetRef, popperElement);
|
||||
|
||||
if (!account) return null;
|
||||
const accountBio = { __html: account.get('note_emojified') };
|
||||
const followedBy = account.getIn(['relationship', 'followed_by']);
|
||||
|
||||
return (
|
||||
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })} ref={setPopperElement} style={styles.popper} {...attributes.popper} onMouseEnter={handleMouseEnter(dispatch)} onMouseLeave={handleMouseLeave(dispatch)}>
|
||||
<div className='profile-hover-card__container'>
|
||||
{followedBy &&
|
||||
<span className='relationship-tag'>
|
||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||
</span>}
|
||||
<div className='profile-hover-card__action-button'><ActionButton account={account} small /></div>
|
||||
<UserPanel className='profile-hover-card__user' accountId={account.get('id')} />
|
||||
{badges.length > 0 &&
|
||||
<div className='profile-hover-card__badges'>
|
||||
{badges}
|
||||
</div>}
|
||||
{account.getIn(['source', 'note'], '').length > 0 &&
|
||||
<div className='profile-hover-card__bio' dangerouslySetInnerHTML={accountBio} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfileHoverCard.propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
accountId: PropTypes.string,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
ProfileHoverCard.defaultProps = {
|
||||
visible: true,
|
||||
};
|
||||
|
||||
export default injectIntl(ProfileHoverCard);
|
|
@ -18,9 +18,8 @@ import classNames from 'classnames';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import PollContainer from 'soapbox/containers/poll_container';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import ProfileHoverCardContainer from '../features/profile_hover_card/profile_hover_card_container';
|
||||
import { isMobile } from '../../../app/soapbox/is_mobile';
|
||||
import { debounce } from 'lodash';
|
||||
import { getDomain } from 'soapbox/utils/accounts';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
|
@ -81,6 +80,7 @@ class Status extends ImmutablePureComponent {
|
|||
onEmbed: PropTypes.func,
|
||||
onHeightChange: PropTypes.func,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onShowHoverProfileCard: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
|
@ -107,7 +107,6 @@ class Status extends ImmutablePureComponent {
|
|||
state = {
|
||||
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
||||
statusId: undefined,
|
||||
profileCardVisible: false,
|
||||
};
|
||||
|
||||
// Track height changes we know about to compensate scrolling
|
||||
|
@ -150,14 +149,16 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.node && this.props.getScrollPosition) {
|
||||
const position = this.props.getScrollPosition();
|
||||
if (position !== null && this.node.offsetTop < position.top) {
|
||||
requestAnimationFrame(() => {
|
||||
this.props.updateScrollBottom(position.height - position.top);
|
||||
});
|
||||
}
|
||||
}
|
||||
// FIXME: Run this code only when a status is being deleted.
|
||||
//
|
||||
// if (this.node && this.props.getScrollPosition) {
|
||||
// const position = this.props.getScrollPosition();
|
||||
// if (position !== null && this.node.offsetTop < position.top) {
|
||||
// requestAnimationFrame(() => {
|
||||
// this.props.updateScrollBottom(position.height - position.top);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
|
@ -253,19 +254,6 @@ class Status extends ImmutablePureComponent {
|
|||
this.handleToggleMediaVisibility();
|
||||
}
|
||||
|
||||
showProfileCard = debounce(() => {
|
||||
this.setState({ profileCardVisible: true });
|
||||
}, 1200);
|
||||
|
||||
handleProfileHover = e => {
|
||||
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
||||
}
|
||||
|
||||
handleProfileLeave = e => {
|
||||
this.showProfileCard.cancel();
|
||||
this.setState({ profileCardVisible: false });
|
||||
}
|
||||
|
||||
_properStatus() {
|
||||
const { status } = this.props;
|
||||
|
||||
|
@ -454,7 +442,8 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
||||
const { profileCardVisible } = this.state;
|
||||
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
||||
const domain = getDomain(status.get('account'));
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
|
@ -468,17 +457,22 @@ class Status extends ImmutablePureComponent {
|
|||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||
</NavLink>
|
||||
|
||||
<div className='status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||
{favicon &&
|
||||
<div className='status__favicon'>
|
||||
<img src={favicon} alt='' title={domain} />
|
||||
</div>}
|
||||
|
||||
<div className='status__profile'>
|
||||
<div className='status__avatar'>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||
{statusAvatar}
|
||||
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])}>
|
||||
{statusAvatar}
|
||||
</NavLink>
|
||||
</HoverRefWrapper>
|
||||
</div>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||
</NavLink>
|
||||
{ profileCardVisible &&
|
||||
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth)} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
exports[`<LoginForm /> renders correctly 1`] = `
|
||||
<form
|
||||
className="simple_form new_user"
|
||||
method="post"
|
||||
>
|
||||
<fieldset>
|
||||
<div
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
exports[`<LoginPage /> renders correctly on load 1`] = `
|
||||
<form
|
||||
className="simple_form new_user"
|
||||
method="post"
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<fieldset
|
||||
|
|
|
@ -17,7 +17,7 @@ class LoginForm extends ImmutablePureComponent {
|
|||
const { intl, isLoading, handleSubmit } = this.props;
|
||||
|
||||
return (
|
||||
<form className='simple_form new_user' onSubmit={handleSubmit}>
|
||||
<form className='simple_form new_user' method='post' onSubmit={handleSubmit}>
|
||||
<fieldset disabled={isLoading}>
|
||||
<div className='fields-group'>
|
||||
<div className='input email optional user_email'>
|
||||
|
|
|
@ -54,7 +54,7 @@ class OtpAuthForm extends ImmutablePureComponent {
|
|||
const { code_error } = this.state;
|
||||
|
||||
return (
|
||||
<form className='simple_form new_user otp-auth' onSubmit={this.handleSubmit}>
|
||||
<form className='simple_form new_user otp-auth' method='post' onSubmit={this.handleSubmit}>
|
||||
<fieldset disabled={this.state.isLoading}>
|
||||
<div className='fields-group'>
|
||||
<div className='input email optional user_email'>
|
||||
|
|
|
@ -27,7 +27,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
|
@ -3,21 +3,24 @@ import { connect } from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import { acctFull } from 'soapbox/utils/accounts';
|
||||
import { fetchChat } from 'soapbox/actions/chats';
|
||||
import { fetchChat, markChatRead } from 'soapbox/actions/chats';
|
||||
import ChatBox from './components/chat_box';
|
||||
import Column from 'soapbox/components/column';
|
||||
import ColumnBackButton from 'soapbox/components/column_back_button';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { makeGetChat } from 'soapbox/selectors';
|
||||
|
||||
const mapStateToProps = (state, { params }) => {
|
||||
const getChat = makeGetChat();
|
||||
const chat = state.getIn(['chats', params.chatId], ImmutableMap()).toJS();
|
||||
|
||||
return {
|
||||
me: state.get('me'),
|
||||
chat: getChat(state, { id: params.chatId }),
|
||||
chat: getChat(state, chat),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -42,9 +45,26 @@ class ChatRoom extends ImmutablePureComponent {
|
|||
this.inputElem.focus();
|
||||
}
|
||||
|
||||
markRead = () => {
|
||||
const { dispatch, chat } = this.props;
|
||||
if (!chat) return;
|
||||
dispatch(markChatRead(chat.get('id')));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, params } = this.props;
|
||||
dispatch(fetchChat(params.chatId));
|
||||
this.markRead();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const markReadConditions = [
|
||||
() => this.props.chat,
|
||||
() => this.props.chat.get('unread') > 0,
|
||||
];
|
||||
|
||||
if (markReadConditions.every(c => c()))
|
||||
this.markRead();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -56,12 +76,12 @@ class ChatRoom extends ImmutablePureComponent {
|
|||
<Column>
|
||||
<div className='chatroom__back'>
|
||||
<ColumnBackButton />
|
||||
<div className='chatroom__header'>
|
||||
<Link to={`/@${account.get('acct')}`} className='chatroom__header'>
|
||||
<Avatar account={account} size={18} />
|
||||
<div className='chatroom__title'>
|
||||
@{acctFull(account)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<ChatBox
|
||||
chatId={chat.get('id')}
|
||||
|
|
|
@ -5,12 +5,16 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import {
|
||||
fetchChatMessages,
|
||||
sendChatMessage,
|
||||
markChatRead,
|
||||
} from 'soapbox/actions/chats';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import ChatMessageList from './chat_message_list';
|
||||
import UploadButton from 'soapbox/features/compose/components/upload_button';
|
||||
import { uploadMedia } from 'soapbox/actions/media';
|
||||
import UploadProgress from 'soapbox/features/compose/components/upload_progress';
|
||||
import { truncateFilename } from 'soapbox/utils/media';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' },
|
||||
|
@ -22,6 +26,8 @@ const mapStateToProps = (state, { chatId }) => ({
|
|||
chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()),
|
||||
});
|
||||
|
||||
const fileKeyGen = () => Math.floor((Math.random() * 0x10000));
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ChatBox extends ImmutablePureComponent {
|
||||
|
@ -36,15 +42,63 @@ class ChatBox extends ImmutablePureComponent {
|
|||
me: PropTypes.node,
|
||||
}
|
||||
|
||||
state = {
|
||||
initialState = () => ({
|
||||
content: '',
|
||||
attachment: undefined,
|
||||
isUploading: false,
|
||||
uploadProgress: 0,
|
||||
resetFileKey: fileKeyGen(),
|
||||
})
|
||||
|
||||
state = this.initialState()
|
||||
|
||||
clearState = () => {
|
||||
this.setState(this.initialState());
|
||||
}
|
||||
|
||||
getParams = () => {
|
||||
const { content, attachment } = this.state;
|
||||
|
||||
return {
|
||||
content,
|
||||
media_id: attachment && attachment.id,
|
||||
};
|
||||
}
|
||||
|
||||
canSubmit = () => {
|
||||
const { content, attachment } = this.state;
|
||||
|
||||
const conds = [
|
||||
content.length > 0,
|
||||
attachment,
|
||||
];
|
||||
|
||||
return conds.some(c => c);
|
||||
}
|
||||
|
||||
sendMessage = () => {
|
||||
const { dispatch, chatId } = this.props;
|
||||
const { isUploading } = this.state;
|
||||
|
||||
if (this.canSubmit() && !isUploading) {
|
||||
const params = this.getParams();
|
||||
|
||||
dispatch(sendChatMessage(chatId, params));
|
||||
this.clearState();
|
||||
}
|
||||
}
|
||||
|
||||
insertLine = () => {
|
||||
const { content } = this.state;
|
||||
this.setState({ content: content + '\n' });
|
||||
}
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
const { chatId } = this.props;
|
||||
if (e.key === 'Enter') {
|
||||
this.props.dispatch(sendChatMessage(chatId, this.state));
|
||||
this.setState({ content: '' });
|
||||
if (e.key === 'Enter' && e.shiftKey) {
|
||||
this.insertLine();
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
@ -68,11 +122,6 @@ class ChatBox extends ImmutablePureComponent {
|
|||
onSetInputRef(el);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, chatId } = this.props;
|
||||
dispatch(fetchChatMessages(chatId));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const markReadConditions = [
|
||||
() => this.props.chat !== undefined,
|
||||
|
@ -84,20 +133,76 @@ class ChatBox extends ImmutablePureComponent {
|
|||
this.markRead();
|
||||
}
|
||||
|
||||
handleRemoveFile = (e) => {
|
||||
this.setState({ attachment: undefined, resetFileKey: fileKeyGen() });
|
||||
}
|
||||
|
||||
onUploadProgress = (e) => {
|
||||
const { loaded, total } = e;
|
||||
this.setState({ uploadProgress: loaded/total });
|
||||
}
|
||||
|
||||
handleFiles = (files) => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
this.setState({ isUploading: true });
|
||||
|
||||
const data = new FormData();
|
||||
data.append('file', files[0]);
|
||||
|
||||
dispatch(uploadMedia(data, this.onUploadProgress)).then(response => {
|
||||
this.setState({ attachment: response.data, isUploading: false });
|
||||
}).catch(() => {
|
||||
this.setState({ isUploading: false });
|
||||
});
|
||||
}
|
||||
|
||||
renderAttachment = () => {
|
||||
const { attachment } = this.state;
|
||||
if (!attachment) return null;
|
||||
|
||||
return (
|
||||
<div className='chat-box__attachment'>
|
||||
<div className='chat-box__filename'>
|
||||
{truncateFilename(attachment.preview_url, 20)}
|
||||
</div>
|
||||
<div class='chat-box__remove-attachment'>
|
||||
<IconButton icon='remove' onClick={this.handleRemoveFile} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderActionButton = () => {
|
||||
const { resetFileKey } = this.state;
|
||||
|
||||
return this.canSubmit() ? (
|
||||
<div className='chat-box__send'>
|
||||
<IconButton icon='send' size={16} onClick={this.sendMessage} />
|
||||
</div>
|
||||
) : (
|
||||
<UploadButton onSelectFile={this.handleFiles} resetFileKey={resetFileKey} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chatMessageIds, intl } = this.props;
|
||||
const { chatMessageIds, chatId, intl } = this.props;
|
||||
const { content, isUploading, uploadProgress } = this.state;
|
||||
if (!chatMessageIds) return null;
|
||||
|
||||
return (
|
||||
<div className='chat-box' onMouseOver={this.handleHover}>
|
||||
<ChatMessageList chatMessageIds={chatMessageIds} />
|
||||
<ChatMessageList chatMessageIds={chatMessageIds} chatId={chatId} />
|
||||
{this.renderAttachment()}
|
||||
<UploadProgress active={isUploading} progress={uploadProgress*100} />
|
||||
<div className='chat-box__actions simple_form'>
|
||||
{this.renderActionButton()}
|
||||
<textarea
|
||||
rows={1}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleContentChange}
|
||||
value={this.state.content}
|
||||
value={content}
|
||||
ref={this.setInputRef}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,6 @@ import { connect } from 'react-redux';
|
|||
import PropTypes from 'prop-types';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { fetchChats } from 'soapbox/actions/chats';
|
||||
import Chat from './chat';
|
||||
import { makeGetChat } from 'soapbox/selectors';
|
||||
|
||||
|
@ -42,10 +41,6 @@ class ChatList extends ImmutablePureComponent {
|
|||
emptyMessage: PropTypes.node,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchChats());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chats, emptyMessage } = this.props;
|
||||
|
||||
|
|
|
@ -4,16 +4,25 @@ import PropTypes from 'prop-types';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { fetchChatMessages } from 'soapbox/actions/chats';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import classNames from 'classnames';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { escape, throttle } from 'lodash';
|
||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis', ImmutableList()).reduce((map, emoji) => {
|
||||
return map.set(`:${emoji.get('shortcode')}:`, emoji);
|
||||
}, ImmutableMap());
|
||||
|
||||
const mapStateToProps = (state, { chatMessageIds }) => ({
|
||||
me: state.get('me'),
|
||||
chatMessages: chatMessageIds.reduce((acc, curr) => {
|
||||
const chatMessage = state.getIn(['chat_messages', curr]);
|
||||
return chatMessage ? acc.push(chatMessage) : acc;
|
||||
}, ImmutableList()).sort(),
|
||||
}, ImmutableList()),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -23,6 +32,7 @@ class ChatMessageList extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
chatId: PropTypes.string,
|
||||
chatMessages: ImmutablePropTypes.list,
|
||||
chatMessageIds: ImmutablePropTypes.orderedSet,
|
||||
me: PropTypes.node,
|
||||
|
@ -32,14 +42,18 @@ class ChatMessageList extends ImmutablePureComponent {
|
|||
chatMessages: ImmutableList(),
|
||||
}
|
||||
|
||||
state = {
|
||||
initialLoad: true,
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
scrollToBottom = () => {
|
||||
if (!this.messagesEnd) return;
|
||||
this.messagesEnd.scrollIntoView();
|
||||
this.messagesEnd.scrollIntoView(false);
|
||||
}
|
||||
|
||||
setMessageEndRef = (el) => {
|
||||
this.messagesEnd = el;
|
||||
this.scrollToBottom();
|
||||
};
|
||||
|
||||
getFormattedTimestamp = (chatMessage) => {
|
||||
|
@ -56,16 +70,132 @@ class ChatMessageList extends ImmutablePureComponent {
|
|||
);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.chatMessages !== this.props.chatMessages)
|
||||
this.scrollToBottom();
|
||||
setBubbleRef = (c) => {
|
||||
if (!c) return;
|
||||
const links = c.querySelectorAll('a[rel="ugc"]');
|
||||
|
||||
links.forEach(link => {
|
||||
link.classList.add('chat-link');
|
||||
link.setAttribute('rel', 'ugc nofollow noopener');
|
||||
link.setAttribute('target', '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
isNearBottom = () => {
|
||||
const elem = this.node;
|
||||
if (!elem) return false;
|
||||
|
||||
const scrollBottom = elem.scrollHeight - elem.offsetHeight - elem.scrollTop;
|
||||
return scrollBottom < elem.offsetHeight * 1.5;
|
||||
}
|
||||
|
||||
handleResize = (e) => {
|
||||
if (this.isNearBottom()) this.scrollToBottom();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, chatId } = this.props;
|
||||
dispatch(fetchChatMessages(chatId));
|
||||
|
||||
this.node.addEventListener('scroll', this.handleScroll);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate(prevProps, prevState) {
|
||||
const { scrollHeight, scrollTop } = this.node;
|
||||
return scrollHeight - scrollTop;
|
||||
}
|
||||
|
||||
restoreScrollPosition = (scrollBottom) => {
|
||||
this.lastComputedScroll = this.node.scrollHeight - scrollBottom;
|
||||
this.node.scrollTop = this.lastComputedScroll;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, scrollBottom) {
|
||||
const { initialLoad } = this.state;
|
||||
const oldCount = prevProps.chatMessages.count();
|
||||
const newCount = this.props.chatMessages.count();
|
||||
const isNearBottom = this.isNearBottom();
|
||||
const historyAdded = prevProps.chatMessages.getIn([0, 'id']) !== this.props.chatMessages.getIn([0, 'id']);
|
||||
|
||||
// Retain scroll bar position when loading old messages
|
||||
this.restoreScrollPosition(scrollBottom);
|
||||
|
||||
if (oldCount !== newCount) {
|
||||
if (isNearBottom || initialLoad) this.scrollToBottom();
|
||||
if (historyAdded) this.setState({ isLoading: false, initialLoad: false });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.node.removeEventListener('scroll', this.handleScroll);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
handleLoadMore = () => {
|
||||
const { dispatch, chatId, chatMessages } = this.props;
|
||||
const maxId = chatMessages.getIn([0, 'id']);
|
||||
dispatch(fetchChatMessages(chatId, maxId));
|
||||
this.setState({ isLoading: true });
|
||||
}
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
const { lastComputedScroll } = this;
|
||||
const { isLoading, initialLoad } = this.state;
|
||||
const { scrollTop, offsetHeight } = this.node;
|
||||
const computedScroll = lastComputedScroll === scrollTop;
|
||||
const nearTop = scrollTop < offsetHeight * 2;
|
||||
|
||||
if (nearTop && !isLoading && !initialLoad && !computedScroll)
|
||||
this.handleLoadMore();
|
||||
}, 150, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
onOpenMedia = (media, index) => {
|
||||
this.props.dispatch(openModal('MEDIA', { media, index }));
|
||||
};
|
||||
|
||||
maybeRenderMedia = chatMessage => {
|
||||
const attachment = chatMessage.get('attachment');
|
||||
if (!attachment) return null;
|
||||
return (
|
||||
<div className='chat-message__media'>
|
||||
<Bundle fetchComponent={MediaGallery}>
|
||||
{Component => (
|
||||
<Component
|
||||
media={ImmutableList([attachment])}
|
||||
height={120}
|
||||
onOpenMedia={this.onOpenMedia}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
parsePendingContent = content => {
|
||||
return escape(content).replace(/(?:\r\n|\r|\n)/g, '<br>');
|
||||
}
|
||||
|
||||
parseContent = chatMessage => {
|
||||
const content = chatMessage.get('content') || '';
|
||||
const pending = chatMessage.get('pending', false);
|
||||
const formatted = pending ? this.parsePendingContent(content) : content;
|
||||
const emojiMap = makeEmojiMap(chatMessage);
|
||||
return emojify(formatted, emojiMap.toJS());
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chatMessages, me } = this.props;
|
||||
|
||||
return (
|
||||
<div className='chat-messages'>
|
||||
<div className='chat-messages' ref={this.setRef}>
|
||||
{chatMessages.map(chatMessage => (
|
||||
<div
|
||||
className={classNames('chat-message', {
|
||||
|
@ -74,11 +204,17 @@ class ChatMessageList extends ImmutablePureComponent {
|
|||
})}
|
||||
key={chatMessage.get('id')}
|
||||
>
|
||||
<span
|
||||
<div
|
||||
title={this.getFormattedTimestamp(chatMessage)}
|
||||
className='chat-message__bubble'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(chatMessage.get('content') || '') }}
|
||||
/>
|
||||
ref={this.setBubbleRef}
|
||||
>
|
||||
{this.maybeRenderMedia(chatMessage)}
|
||||
<span
|
||||
className='chat-message__content'
|
||||
dangerouslySetInnerHTML={{ __html: this.parseContent(chatMessage) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ float: 'left', clear: 'both' }} ref={this.setMessageEndRef} />
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from 'soapbox/actions/chats';
|
||||
import ChatBox from './chat_box';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
|
||||
const mapStateToProps = (state, { pane }) => ({
|
||||
me: state.get('me'),
|
||||
|
@ -79,13 +80,24 @@ class ChatWindow extends ImmutablePureComponent {
|
|||
const right = (285 * (idx + 1)) + 20;
|
||||
const unreadCount = chat.get('unread');
|
||||
|
||||
const unreadIcon = (
|
||||
<i className='icon-with-badge__badge'>
|
||||
{shortNumberFormat(unreadCount)}
|
||||
</i>
|
||||
);
|
||||
|
||||
const avatar = (
|
||||
<HoverRefWrapper accountId={account.get('id')}>
|
||||
<Link to={`/@${account.get('acct')}`}>
|
||||
<Avatar account={account} size={18} />
|
||||
</Link>
|
||||
</HoverRefWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`pane pane--${pane.get('state')}`} style={{ right: `${right}px` }}>
|
||||
<div className='pane__header'>
|
||||
{unreadCount > 0
|
||||
? <i className='icon-with-badge__badge'>{shortNumberFormat(unreadCount)}</i>
|
||||
: <Link to={`/@${account.get('acct')}`}><Avatar account={account} size={18} /></Link>
|
||||
}
|
||||
{unreadCount > 0 ? unreadIcon : avatar }
|
||||
<button className='pane__title' onClick={this.handleChatToggle(chat.get('id'))}>
|
||||
@{acctFull(account)}
|
||||
</button>
|
||||
|
|
|
@ -39,7 +39,7 @@ class PollButton extends React.PureComponent {
|
|||
return (
|
||||
<div className='compose-form__poll-button'>
|
||||
<IconButton
|
||||
icon='tasks'
|
||||
icon='bar-chart'
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
|
|
|
@ -38,6 +38,8 @@ class Option extends React.PureComponent {
|
|||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
maxChars: PropTypes.number.isRequired,
|
||||
onRemovePoll: PropTypes.func.isRequired,
|
||||
numOptions: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
handleOptionTitleChange = e => {
|
||||
|
@ -45,10 +47,12 @@ class Option extends React.PureComponent {
|
|||
};
|
||||
|
||||
handleOptionRemove = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
if (this.props.numOptions > 2)
|
||||
this.props.onRemove(this.props.index);
|
||||
else
|
||||
this.props.onRemovePoll();
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
|
@ -95,7 +99,7 @@ class Option extends React.PureComponent {
|
|||
</label>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
|
||||
<IconButton title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -156,6 +160,7 @@ class PollForm extends ImmutablePureComponent {
|
|||
isPollMultiple={isMultiple}
|
||||
onToggleMultiple={this.handleToggleMultiple}
|
||||
maxChars={maxOptionChars}
|
||||
numOptions={options.size}
|
||||
{...other}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -173,7 +173,7 @@ class PrivacyDropdown extends React.PureComponent {
|
|||
const { intl: { formatMessage } } = props;
|
||||
|
||||
this.options = [
|
||||
{ icon: 'globe', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'globe-w', value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
{ icon: 'envelope', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
|
||||
|
|
|
@ -52,7 +52,7 @@ class UploadButton extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
|
||||
const { intl, resetFileKey, unavailable, disabled } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
|
@ -60,7 +60,7 @@ class UploadButton extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='upload' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<IconButton icon='paperclip' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.upload)}</span>
|
||||
<input
|
||||
|
@ -68,7 +68,8 @@ class UploadButton extends ImmutablePureComponent {
|
|||
ref={this.setRef}
|
||||
type='file'
|
||||
multiple
|
||||
accept={acceptContentTypes.toArray().join(',')}
|
||||
// Accept all types for now.
|
||||
// accept={acceptContentTypes.toArray().join(',')}
|
||||
onChange={this.handleChange}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PollForm from '../components/poll_form';
|
||||
import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
|
||||
import { addPollOption, removePollOption, changePollOption, changePollSettings, removePoll } from '../../../actions/compose';
|
||||
import {
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
|
@ -43,6 +43,10 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(selectComposeSuggestion(position, token, accountId, path));
|
||||
},
|
||||
|
||||
onRemovePoll() {
|
||||
dispatch(removePoll());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
|
||||
|
|
|
@ -88,7 +88,7 @@ class Compose extends React.PureComponent {
|
|||
<Link to='/timelines/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' fixedWidth /></Link>
|
||||
)}
|
||||
{!columns.some(column => column.get('id') === 'PUBLIC') && (
|
||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' fixedWidth /></Link>
|
||||
<Link to='/timelines/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe-w' fixedWidth /></Link>
|
||||
)}
|
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' fixedWidth /></a>
|
||||
<a href='/auth/sign_out' className='drawer__tab' data-method='delete' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)}><Icon id='sign-out' fixedWidth /></a>
|
||||
|
|
|
@ -20,7 +20,7 @@ class Favourites extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -28,7 +28,7 @@ class FollowRequests extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ class Followers extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
diffCount: PropTypes.number,
|
||||
isAccount: PropTypes.bool,
|
||||
|
|
|
@ -51,7 +51,7 @@ class Following extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
|
|
|
@ -78,6 +78,7 @@ exports[`<SimpleForm /> renders correctly 1`] = `
|
|||
<form
|
||||
acceptCharset="UTF-8"
|
||||
className="simple_form"
|
||||
method="post"
|
||||
onSubmit={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -146,7 +146,7 @@ export class SimpleForm extends ImmutablePureComponent {
|
|||
render() {
|
||||
const { children, onSubmit, ...props } = this.props;
|
||||
return (
|
||||
<form className='simple_form' onSubmit={this.onSubmit} {...props}>
|
||||
<form className='simple_form' method='post' onSubmit={this.onSubmit} {...props}>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -75,7 +75,7 @@ class Create extends React.PureComponent {
|
|||
const { title, description, coverImage, disabled, intl } = this.props;
|
||||
|
||||
return (
|
||||
<form className='group-form' onSubmit={this.handleSubmit}>
|
||||
<form className='group-form' method='post' onSubmit={this.handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
className='standard'
|
||||
|
|
|
@ -105,7 +105,7 @@ class Edit extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<form className='group-form' onSubmit={this.handleSubmit}>
|
||||
<form className='group-form' method='post' onSubmit={this.handleSubmit}>
|
||||
<div>
|
||||
<input
|
||||
className='standard'
|
||||
|
|
|
@ -26,7 +26,7 @@ class GroupMembers extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ class GroupRemovedAccounts extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
hasMore: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ class ListForm extends React.PureComponent {
|
|||
const save = intl.formatMessage(messages.save);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<form className='column-inline-form' method='post' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text new-list-form__input'
|
||||
value={value}
|
||||
|
|
|
@ -53,7 +53,7 @@ class NewListForm extends React.PureComponent {
|
|||
const create = intl.formatMessage(messages.create);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<form className='column-inline-form' method='post' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ class Mutes extends ImmutablePureComponent {
|
|||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from '../../selectors';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UserPanel from '../ui/components/user_panel';
|
||||
import ActionButton from '../ui/components/action_button';
|
||||
import { isAdmin, isModerator } from 'soapbox/utils/accounts';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import classNames from 'classnames';
|
||||
import { fetchRelationships } from 'soapbox/actions/accounts';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => {
|
||||
return {
|
||||
account: getAccount(state, accountId),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class ProfileHoverCardContainer extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
accountId: PropTypes.string,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
visible: true,
|
||||
}
|
||||
|
||||
getBadges = () => {
|
||||
const { account } = this.props;
|
||||
let badges = [];
|
||||
if (isAdmin(account)) badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
||||
if (isModerator(account)) badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
||||
if (account.getIn(['patron', 'is_patron'])) badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
||||
return badges;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchRelationships([this.props.accountId]));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visible, accountId, account } = this.props;
|
||||
if (!accountId) return null;
|
||||
const accountBio = { __html: account.get('note_emojified') };
|
||||
const followedBy = account.getIn(['relationship', 'followed_by']);
|
||||
const badges = this.getBadges();
|
||||
|
||||
return (
|
||||
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })}>
|
||||
<div className='profile-hover-card__container'>
|
||||
{followedBy &&
|
||||
<span className='relationship-tag'>
|
||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||
</span>}
|
||||
<div className='profile-hover-card__action-button'><ActionButton account={account} small /></div>
|
||||
<UserPanel className='profile-hover-card__user' accountId={accountId} />
|
||||
{badges.length > 0 &&
|
||||
<div className='profile-hover-card__badges'>
|
||||
{badges}
|
||||
</div>}
|
||||
{account.getIn(['source', 'note'], '').length > 0 &&
|
||||
<div className='profile-hover-card__bio' dangerouslySetInnerHTML={accountBio} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
|
@ -32,7 +32,7 @@ class Reblogs extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
accountIds: ImmutablePropTypes.orderedSet,
|
||||
status: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
|
|
|
@ -34,6 +34,8 @@ const messages = defineMessages({
|
|||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
@ -63,6 +65,7 @@ class ActionBar extends React.PureComponent {
|
|||
onFavourite: PropTypes.func.isRequired,
|
||||
onEmojiReact: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onBookmark: PropTypes.func,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func,
|
||||
|
@ -103,6 +106,10 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleBookmarkClick = () => {
|
||||
this.props.onBookmark(this.props.status);
|
||||
}
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
const { me } = this.props;
|
||||
if (me) {
|
||||
|
@ -237,9 +244,12 @@ class ActionBar extends React.PureComponent {
|
|||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||
// menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (me === status.getIn(['account', 'id'])) {
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
|
|
|
@ -16,9 +16,8 @@ import classNames from 'classnames';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import PollContainer from 'soapbox/containers/poll_container';
|
||||
import { StatusInteractionBar } from './status_interaction_bar';
|
||||
import ProfileHoverCardContainer from 'soapbox/features/profile_hover_card/profile_hover_card_container';
|
||||
import { isMobile } from 'soapbox/is_mobile';
|
||||
import { debounce } from 'lodash';
|
||||
import { getDomain } from 'soapbox/utils/accounts';
|
||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||
|
||||
export default class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
|
@ -41,7 +40,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
state = {
|
||||
height: null,
|
||||
profileCardVisible: false,
|
||||
};
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
|
@ -85,24 +83,12 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||
}
|
||||
|
||||
showProfileCard = debounce(() => {
|
||||
this.setState({ profileCardVisible: true });
|
||||
}, 1200);
|
||||
|
||||
handleProfileHover = e => {
|
||||
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
||||
}
|
||||
|
||||
handleProfileLeave = e => {
|
||||
this.showProfileCard.cancel();
|
||||
this.setState({ profileCardVisible: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||
const outerStyle = { boxSizing: 'border-box' };
|
||||
const { compact } = this.props;
|
||||
const { profileCardVisible } = this.state;
|
||||
const favicon = status.getIn(['account', 'pleroma', 'favicon']);
|
||||
const domain = getDomain(status.get('account'));
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
|
@ -178,20 +164,21 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
return (
|
||||
<div style={outerStyle}>
|
||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
||||
<div className='detailed-status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||
<div className='detailed-status__profile'>
|
||||
<div className='detailed-status__display-name'>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`}>
|
||||
<div className='detailed-status__display-avatar'>
|
||||
<Avatar account={status.get('account')} size={48} />
|
||||
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||
<Avatar account={status.get('account')} size={48} />
|
||||
</HoverRefWrapper>
|
||||
</div>
|
||||
</NavLink>
|
||||
<DisplayName account={status.get('account')}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||
<HoverRefWrapper accountId={status.getIn(['account', 'id'])}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} />
|
||||
</HoverRefWrapper>
|
||||
</DisplayName>
|
||||
</div>
|
||||
{ profileCardVisible &&
|
||||
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth) && profileCardVisible} />
|
||||
}
|
||||
</div>
|
||||
|
||||
{status.get('group') && (
|
||||
|
@ -208,6 +195,11 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
<div className='detailed-status__meta'>
|
||||
<StatusInteractionBar status={status} />
|
||||
<div>
|
||||
{favicon &&
|
||||
<div className='status__favicon'>
|
||||
<img src={favicon} alt='' title={domain} />
|
||||
</div>}
|
||||
|
||||
{statusTypeIcon}<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
|
||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||
</a>
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
favourite,
|
||||
unreblog,
|
||||
unfavourite,
|
||||
bookmark,
|
||||
unbookmark,
|
||||
pin,
|
||||
unpin,
|
||||
} from '../../../actions/interactions';
|
||||
|
@ -88,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
});
|
||||
},
|
||||
|
||||
onBookmark(status) {
|
||||
if (status.get('bookmarked')) {
|
||||
dispatch(unbookmark(status));
|
||||
} else {
|
||||
dispatch(bookmark(status));
|
||||
}
|
||||
},
|
||||
|
||||
onFavourite(status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
unfavourite,
|
||||
reblog,
|
||||
unreblog,
|
||||
bookmark,
|
||||
unbookmark,
|
||||
pin,
|
||||
unpin,
|
||||
} from '../../actions/interactions';
|
||||
|
@ -168,6 +170,14 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleBookmark = (status) => {
|
||||
if (status.get('bookmarked')) {
|
||||
this.props.dispatch(unbookmark(status));
|
||||
} else {
|
||||
this.props.dispatch(bookmark(status));
|
||||
}
|
||||
}
|
||||
|
||||
handleReplyClick = (status) => {
|
||||
let { askReplyConfirmation, dispatch, intl } = this.props;
|
||||
if (askReplyConfirmation) {
|
||||
|
@ -408,7 +418,7 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (prevProps.status && ancestorsIds && ancestorsIds.size > 0) {
|
||||
const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
|
||||
const element = this.node.querySelector('.detailed-status');
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
element.scrollIntoView(true);
|
||||
|
@ -507,6 +517,7 @@ class Status extends ImmutablePureComponent {
|
|||
onBlock={this.handleBlockClick}
|
||||
onReport={this.handleReport}
|
||||
onPin={this.handlePin}
|
||||
onBookmark={this.handleBookmark}
|
||||
onEmbed={this.handleEmbed}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -46,6 +46,11 @@ class TabsBar extends React.PureComponent {
|
|||
this.node = ref;
|
||||
}
|
||||
|
||||
isHomeActive = (match, location) => {
|
||||
const { pathname } = location;
|
||||
return pathname === '/' || pathname.startsWith('/timeline/');
|
||||
}
|
||||
|
||||
getNavLinks() {
|
||||
const { intl: { formatMessage }, logo, account } = this.props;
|
||||
let links = [];
|
||||
|
@ -57,7 +62,7 @@ class TabsBar extends React.PureComponent {
|
|||
</Link>);
|
||||
}
|
||||
links.push(
|
||||
<NavLink key='home' className='tabs-bar__link' exact to='/' data-preview-title-id='column.home'>
|
||||
<NavLink key='home' className='tabs-bar__link' exact to='/' data-preview-title-id='column.home' isActive={this.isHomeActive}>
|
||||
<Icon id='home' />
|
||||
<span><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></span>
|
||||
</NavLink>);
|
||||
|
|
|
@ -56,26 +56,26 @@ class UserPanel extends ImmutablePureComponent {
|
|||
|
||||
<div className='user-panel__stats-block'>
|
||||
|
||||
<div className='user-panel-stats-item'>
|
||||
{account.get('statuses_count') && <div className='user-panel-stats-item'>
|
||||
<Link to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||
<strong className='user-panel-stats-item__value'>{shortNumberFormat(account.get('statuses_count'))}</strong>
|
||||
<span className='user-panel-stats-item__label'><FormattedMessage className='user-panel-stats-item__label' id='account.posts' defaultMessage='Posts' /></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className='user-panel-stats-item'>
|
||||
{account.get('followers_count') && <div className='user-panel-stats-item'>
|
||||
<Link to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<strong className='user-panel-stats-item__value'>{shortNumberFormat(account.get('followers_count'))}</strong>
|
||||
<span className='user-panel-stats-item__label'><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<div className='user-panel-stats-item'>
|
||||
{account.get('following_count') && <div className='user-panel-stats-item'>
|
||||
<Link to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||
<strong className='user-panel-stats-item__value'>{shortNumberFormat(account.get('following_count'))}</strong>
|
||||
<span className='user-panel-stats-item__label'><FormattedMessage className='user-panel-stats-item__label' id='account.follows' defaultMessage='Follows' /></span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import { expandHomeTimeline } from '../../actions/timelines';
|
|||
import { expandNotifications } from '../../actions/notifications';
|
||||
import { fetchReports } from '../../actions/admin';
|
||||
import { fetchFilters } from '../../actions/filters';
|
||||
import { fetchChats } from 'soapbox/actions/chats';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { openModal } from '../../actions/modal';
|
||||
import { WrappedRoute } from './util/react_router_helpers';
|
||||
|
@ -37,6 +38,7 @@ import { Redirect } from 'react-router-dom';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import { isStaff } from 'soapbox/utils/accounts';
|
||||
import ChatPanes from 'soapbox/features/chats/components/chat_panes';
|
||||
import ProfileHoverCard from 'soapbox/components/profile_hover_card';
|
||||
|
||||
import {
|
||||
Status,
|
||||
|
@ -433,6 +435,7 @@ class UI extends React.PureComponent {
|
|||
if (account) {
|
||||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
this.props.dispatch(fetchChats());
|
||||
// this.props.dispatch(fetchGroups('member'));
|
||||
if (isStaff(account))
|
||||
this.props.dispatch(fetchReports({ state: 'open' }));
|
||||
|
@ -648,6 +651,7 @@ class UI extends React.PureComponent {
|
|||
<UploadArea active={draggingOver} onClose={this.closeUploadModal} />
|
||||
{me && <SidebarMenu />}
|
||||
{me && !mobile && <ChatPanes />}
|
||||
<ProfileHoverCard />
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
|
|
@ -9,9 +9,15 @@ import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutabl
|
|||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
const idComparator = (a, b) => {
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const updateList = (state, chatId, messageIds) => {
|
||||
const ids = state.get(chatId, ImmutableOrderedSet());
|
||||
const newIds = ids.union(messageIds);
|
||||
const newIds = ids.union(messageIds).sort(idComparator);
|
||||
return state.set(chatId, newIds);
|
||||
};
|
||||
|
||||
|
@ -31,22 +37,28 @@ const importLastMessages = (state, chats) =>
|
|||
if (chat.last_message) importMessage(mutable, chat.last_message);
|
||||
}));
|
||||
|
||||
const replaceMessage = (state, chatId, oldId, newId) => {
|
||||
const ids = state.get(chatId, ImmutableOrderedSet());
|
||||
const newIds = ids.delete(oldId).add(newId).sort(idComparator);
|
||||
return state.set(chatId, newIds);
|
||||
};
|
||||
|
||||
export default function chatMessageLists(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case CHAT_MESSAGE_SEND_REQUEST:
|
||||
return updateList(state, action.chatId, [action.uuid]).sort();
|
||||
return updateList(state, action.chatId, [action.uuid]);
|
||||
case CHATS_FETCH_SUCCESS:
|
||||
return importLastMessages(state, action.chats).sort();
|
||||
return importLastMessages(state, action.chats);
|
||||
case STREAMING_CHAT_UPDATE:
|
||||
if (action.chat.last_message &&
|
||||
action.chat.last_message.account_id !== action.me)
|
||||
return importMessages(state, [action.chat.last_message]).sort();
|
||||
return importMessages(state, [action.chat.last_message]);
|
||||
else
|
||||
return state;
|
||||
case CHAT_MESSAGES_FETCH_SUCCESS:
|
||||
return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id).reverse()).sort();
|
||||
return updateList(state, action.chatId, action.chatMessages.map(chat => chat.id));
|
||||
case CHAT_MESSAGE_SEND_SUCCESS:
|
||||
return updateList(state, action.chatId, [action.chatMessage.id]).sort();
|
||||
return replaceMessage(state, action.chatId, action.uuid, action.chatMessage.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import admin from './admin';
|
|||
import chats from './chats';
|
||||
import chat_messages from './chat_messages';
|
||||
import chat_message_lists from './chat_message_lists';
|
||||
import profile_hover_card from './profile_hover_card';
|
||||
|
||||
const reducers = {
|
||||
dropdown_menu,
|
||||
|
@ -95,6 +96,7 @@ const reducers = {
|
|||
chats,
|
||||
chat_messages,
|
||||
chat_message_lists,
|
||||
profile_hover_card,
|
||||
};
|
||||
|
||||
export default combineReducers(reducers);
|
||||
|
|
|
@ -61,8 +61,9 @@ const normalizeNotification = (state, notification) => {
|
|||
const expandNormalizedNotifications = (state, notifications, next) => {
|
||||
let items = ImmutableList();
|
||||
|
||||
notifications.forEach((n, i) => {
|
||||
items = items.set(i, notificationToMap(n));
|
||||
notifications.forEach((n) => {
|
||||
if (!n.account.id) return;
|
||||
items = items.push(notificationToMap(n));
|
||||
});
|
||||
|
||||
return state.withMutations(mutable => {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
PROFILE_HOVER_CARD_OPEN,
|
||||
PROFILE_HOVER_CARD_CLOSE,
|
||||
PROFILE_HOVER_CARD_UPDATE,
|
||||
} from 'soapbox/actions/profile_hover_card';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
export default function profileHoverCard(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case PROFILE_HOVER_CARD_OPEN:
|
||||
return ImmutableMap({
|
||||
ref: action.ref,
|
||||
accountId: action.accountId,
|
||||
});
|
||||
case PROFILE_HOVER_CARD_UPDATE:
|
||||
return state.set('hovered', true);
|
||||
case PROFILE_HOVER_CARD_CLOSE:
|
||||
if (state.get('hovered') === true && !action.force)
|
||||
return state;
|
||||
else
|
||||
return ImmutableMap();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,17 @@
|
|||
import { ADMIN_CONFIG_UPDATE_SUCCESS } from '../actions/admin';
|
||||
import { SOAPBOX_CONFIG_REQUEST_SUCCESS } from '../actions/soapbox';
|
||||
import {
|
||||
SOAPBOX_CONFIG_REQUEST_SUCCESS,
|
||||
SOAPBOX_CONFIG_REQUEST_FAIL,
|
||||
} from '../actions/soapbox';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { ConfigDB } from 'soapbox/utils/config_db';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
const fallbackState = ImmutableMap({
|
||||
brandColor: '#0482d8', // Azure
|
||||
});
|
||||
|
||||
const updateFromAdmin = (state, config) => {
|
||||
const configs = config.get('configs', ImmutableList());
|
||||
|
||||
|
@ -22,6 +29,8 @@ export default function soapbox(state = initialState, action) {
|
|||
switch(action.type) {
|
||||
case SOAPBOX_CONFIG_REQUEST_SUCCESS:
|
||||
return fromJS(action.soapboxConfig);
|
||||
case SOAPBOX_CONFIG_REQUEST_FAIL:
|
||||
return fallbackState.mergeDeep(state);
|
||||
case ADMIN_CONFIG_UPDATE_SUCCESS:
|
||||
return updateFromAdmin(state, fromJS(action.config));
|
||||
default:
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
MUTES_FETCH_SUCCESS,
|
||||
MUTES_EXPAND_SUCCESS,
|
||||
} from '../actions/mutes';
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import {
|
||||
GROUP_MEMBERS_FETCH_SUCCESS,
|
||||
GROUP_MEMBERS_EXPAND_SUCCESS,
|
||||
|
@ -44,7 +44,7 @@ const initialState = ImmutableMap({
|
|||
const normalizeList = (state, type, id, accounts, next) => {
|
||||
return state.setIn([type, id], ImmutableMap({
|
||||
next,
|
||||
items: ImmutableList(accounts.map(item => item.id)),
|
||||
items: ImmutableOrderedSet(accounts.map(item => item.id)),
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -65,22 +65,22 @@ export default function userLists(state = initialState, action) {
|
|||
case FOLLOWING_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'following', action.id, action.accounts, action.next);
|
||||
case REBLOGS_FETCH_SUCCESS:
|
||||
return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||
return state.setIn(['reblogged_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
|
||||
case FAVOURITES_FETCH_SUCCESS:
|
||||
return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id)));
|
||||
return state.setIn(['favourited_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||
return state.setIn(['follow_requests', 'items'], ImmutableOrderedSet(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||
case FOLLOW_REQUESTS_EXPAND_SUCCESS:
|
||||
return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next);
|
||||
case FOLLOW_REQUEST_AUTHORIZE_SUCCESS:
|
||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||
return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id));
|
||||
case BLOCKS_FETCH_SUCCESS:
|
||||
return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case BLOCKS_EXPAND_SUCCESS:
|
||||
return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next);
|
||||
case MUTES_FETCH_SUCCESS:
|
||||
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
return state.setIn(['mutes', 'items'], ImmutableOrderedSet(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case MUTES_EXPAND_SUCCESS:
|
||||
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
|
||||
case GROUP_MEMBERS_FETCH_SUCCESS:
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
const guessDomain = account => {
|
||||
try {
|
||||
let re = /https?:\/\/(.*?)\//i;
|
||||
return re.exec(account.get('url'))[1];
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDomain = account => {
|
||||
let re = /https?:\/\/(.*?)\//i;
|
||||
return re.exec(account.get('url'))[1];
|
||||
let domain = account.get('acct').split('@')[1];
|
||||
if (!domain) domain = guessDomain(account);
|
||||
return domain;
|
||||
};
|
||||
|
||||
// user@domain even for local users
|
||||
export const acctFull = account => {
|
||||
let [user, domain] = account.get('acct').split('@');
|
||||
try {
|
||||
if (!domain) domain = getDomain(account);
|
||||
} catch(e) {
|
||||
console.warning('Could not get domain for acctFull. Falling back to acct.');
|
||||
return account.get('acct');
|
||||
}
|
||||
return [user, domain].join('@');
|
||||
const [user, domain] = account.get('acct').split('@');
|
||||
if (!domain) return [user, guessDomain(account)].join('@');
|
||||
return account.get('acct');
|
||||
};
|
||||
|
||||
export const isStaff = (account = ImmutableMap()) => (
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export const truncateFilename = (url, maxLength) => {
|
||||
const filename = url.split('/').pop();
|
||||
|
||||
if (filename.length <= maxLength) return filename;
|
||||
|
||||
return [
|
||||
filename.substr(0, maxLength/2),
|
||||
filename.substr(filename.length - maxLength/2),
|
||||
].join('…');
|
||||
};
|
|
@ -27,7 +27,6 @@
|
|||
@import 'dyslexic';
|
||||
@import 'demetricator';
|
||||
@import 'pro';
|
||||
@import 'overflow_hacks';
|
||||
@import 'chats';
|
||||
|
||||
// COMPONENTS
|
||||
|
|
|
@ -113,6 +113,8 @@
|
|||
background-color: var(--background-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: break-word;
|
||||
white-space: break-spaces;
|
||||
|
||||
a {
|
||||
color: var(--brand-color--hicontrast);
|
||||
|
@ -150,6 +152,10 @@
|
|||
.display-name {
|
||||
display: flex;
|
||||
|
||||
.hover-ref-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
bdi {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -173,10 +179,65 @@
|
|||
}
|
||||
|
||||
.chat-box {
|
||||
.upload-progress {
|
||||
padding: 0 10px;
|
||||
align-items: center;
|
||||
height: 25px;
|
||||
|
||||
.fa {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&__message {
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&__attachment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
padding: 0 10px;
|
||||
height: 25px;
|
||||
|
||||
.chat-box__remove-attachment {
|
||||
margin-left: auto;
|
||||
|
||||
.icon-button > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
background: var(--foreground-color);
|
||||
margin-top: auto;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
|
||||
.icon-button {
|
||||
color: var(--primary-text-color--faint);
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: calc(50% - 13px);
|
||||
width: auto;
|
||||
height: auto;
|
||||
background: transparent !important;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-box__send .icon-button {
|
||||
top: calc(50% - 9px);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
|
@ -184,11 +245,13 @@
|
|||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
padding-right: 25px;
|
||||
background: var(--background-color);
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: var(--primary-text-color);
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -215,7 +278,38 @@
|
|||
border-radius: 0 0 10px 10px;
|
||||
|
||||
&__actions textarea {
|
||||
padding: 10px;
|
||||
padding: 10px 40px 10px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media(max-width: 630px) {
|
||||
.columns-area__panels__main .columns-area {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.columns-area__panels__main {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.columns-area--mobile .column {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
.chat-box {
|
||||
border-radius: 0;
|
||||
border: 2px solid var(--foreground-color);
|
||||
|
||||
&__actions {
|
||||
padding: 0;
|
||||
|
||||
textarea {
|
||||
height: 4em;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,6 +332,7 @@
|
|||
margin-left: auto;
|
||||
padding-right: 15px;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
|
||||
.account__avatar {
|
||||
margin-right: 7px;
|
||||
|
@ -252,7 +347,7 @@
|
|||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
color: #fff;
|
||||
color: var(--primary-text-color);
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
|
@ -263,7 +358,6 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--accent-color--faint);
|
||||
border-radius: 10px 10px 0 0;
|
||||
|
||||
.column-back-button {
|
||||
background: transparent;
|
||||
|
@ -276,5 +370,37 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
a {
|
||||
color: var(--highlight-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message__media {
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.chat-message .media-gallery {
|
||||
height: 100% !important;
|
||||
margin: 4px 0 8px;
|
||||
|
||||
.spoiler-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.media-gallery__item:not(.media-gallery__item--image) {
|
||||
max-width: 100%;
|
||||
width: 120px !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&__item-thumbnail img,
|
||||
&__item-thumbnail .still-image img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -212,7 +212,6 @@
|
|||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
border: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
text-align: unset;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
|
|
|
@ -62,7 +62,6 @@
|
|||
border-bottom: 1px solid var(--brand-color--faint);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.detailed-status__link {
|
||||
|
@ -125,3 +124,8 @@
|
|||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.detailed-status .status__favicon {
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
.column,
|
||||
.drawer {
|
||||
flex: 1 1 100%;
|
||||
overflow: visible;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
|
|
|
@ -134,6 +134,7 @@
|
|||
}
|
||||
|
||||
.media-gallery__gifv__label,
|
||||
.media-gallery__filename__label,
|
||||
.media-gallery__file-extension__label {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
|
|
@ -15,19 +15,14 @@
|
|||
transition-duration: 0.2s;
|
||||
width: 320px;
|
||||
z-index: 200;
|
||||
left: -10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@media(min-width: 750px) {
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
.profile-hover-card__container {
|
||||
@include standard-panel;
|
||||
position: relative;
|
||||
|
@ -110,22 +105,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status {
|
||||
.profile-hover-card {
|
||||
top: 0;
|
||||
left: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent floating avatars from intercepting with current card */
|
||||
.status,
|
||||
.detailed-status {
|
||||
.floating-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .floating-link {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -635,3 +635,15 @@ a.status-card.compact:hover {
|
|||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.status__favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
float: right;
|
||||
margin-right: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,6 +143,10 @@
|
|||
.setting-toggle {
|
||||
margin-left: 10px;
|
||||
|
||||
.react-toggle-track {
|
||||
background-color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.react-toggle--checked {
|
||||
.react-toggle-track {
|
||||
background-color: var(--accent-color);
|
||||
|
|
|
@ -9,10 +9,6 @@
|
|||
.react-toggle {
|
||||
vertical-align: middle;
|
||||
|
||||
&-track {
|
||||
background-color: var(--foreground-color);
|
||||
}
|
||||
|
||||
&-track-check,
|
||||
&-track-x {
|
||||
display: flex;
|
||||
|
|
|
@ -188,7 +188,6 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
// This is a file dedicated to fixing the css we broke by introducing the hover
|
||||
// card and `overflow:visible` on drawer.scss line 23. If we ever figure out how
|
||||
// to pop the hover card out while keeping `overflow:hidden`, feel free to delete
|
||||
// this entire file.
|
||||
|
||||
button.column-header__button.active {
|
||||
border-radius: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.column-back-button.column-back-button--slim-button {
|
||||
border-radius: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.detailed-status__wrapper .detailed-status__action-bar {
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.slist .item-list .column-link {
|
||||
background-color: transparent;
|
||||
border-top: 1px solid var(--brand-color--med);
|
||||
}
|
||||
|
||||
.focusable {
|
||||
&:focus {
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more:hover {
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
// this still looks like shit but at least it's better than it overflowing
|
||||
|
||||
.empty-column-indicator {
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
|
@ -46,6 +46,7 @@
|
|||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/runtime": "^7.3.4",
|
||||
"@clusterws/cws": "^0.16.0",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"array-includes": "^3.0.3",
|
||||
"autoprefixer": "^9.5.1",
|
||||
"axios": "^0.19.0",
|
||||
|
@ -114,7 +115,8 @@
|
|||
"react-motion": "^0.5.2",
|
||||
"react-notification": "^6.8.4",
|
||||
"react-overlays": "^0.8.3",
|
||||
"react-redux": "^6.0.1",
|
||||
"react-popper": "^2.2.3",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-redux-loading-bar": "^4.5.0",
|
||||
"react-router-dom": "^4.1.1",
|
||||
"react-router-scroll-4": "^1.0.0-beta.1",
|
||||
|
@ -124,7 +126,7 @@
|
|||
"react-textarea-autosize": "^7.1.0",
|
||||
"react-toggle": "^4.0.1",
|
||||
"redis": "^2.7.1",
|
||||
"redux": "^4.0.1",
|
||||
"redux": "^4.0.5",
|
||||
"redux-immutable": "^4.0.0",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"rellax": "^1.7.1",
|
||||
|
@ -138,7 +140,7 @@
|
|||
"substring-trie": "^1.0.2",
|
||||
"throng": "^4.0.0",
|
||||
"tiny-queue": "^0.2.1",
|
||||
"uglifyjs-webpack-plugin": "^2.1.2",
|
||||
"uglifyjs-webpack-plugin": "^2.2.0",
|
||||
"uuid": "^3.1.0",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-assets-manifest": "^3.1.1",
|
||||
|
|
|
@ -40,9 +40,7 @@ module.exports = merge(sharedConfig, {
|
|||
sourceMap: true,
|
||||
|
||||
uglifyOptions: {
|
||||
compress: {
|
||||
warnings: false,
|
||||
},
|
||||
warnings: false,
|
||||
|
||||
output: {
|
||||
comments: false,
|
||||
|
|
108
yarn.lock
108
yarn.lock
|
@ -978,7 +978,7 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2":
|
||||
version "7.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
|
||||
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
|
||||
|
@ -992,6 +992,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.5.5":
|
||||
version "7.11.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
|
||||
integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.7.2":
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
|
||||
|
@ -1482,6 +1489,11 @@
|
|||
"@types/yargs" "^15.0.0"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@popperjs/core@^2.4.4":
|
||||
version "2.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.4.tgz#11d5db19bd178936ec89cd84519c4de439574398"
|
||||
integrity sha512-1oO6+dN5kdIA3sKPZhRGJTfGVP4SWV6KqlMOwry4J3HfyD68sl/3KmG7DeYUzvN+RbhXDnv/D8vNNB8168tAMg==
|
||||
|
||||
"@sinonjs/commons@^1.7.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
|
||||
|
@ -3257,11 +3269,6 @@ commander@^4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
|
||||
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
|
||||
|
||||
commander@~2.17.1:
|
||||
version "2.17.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
|
||||
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
|
||||
|
||||
commondir@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
|
||||
|
@ -9488,6 +9495,11 @@ react-fast-compare@^2.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-fast-compare@^3.0.1:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||
|
||||
react-helmet@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.0.0.tgz#fcb93ebaca3ba562a686eb2f1f9d46093d83b5f8"
|
||||
|
@ -9563,12 +9575,12 @@ react-intl@^4.6.6:
|
|||
intl-messageformat-parser "^5.1.1"
|
||||
shallow-equal "^1.2.1"
|
||||
|
||||
react-is@^16.12.0, react-is@^16.8.6:
|
||||
react-is@^16.12.0, react-is@^16.8.6, react-is@^16.9.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^16.3.2, react-is@^16.6.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.2:
|
||||
react-is@^16.3.2, react-is@^16.6.1, react-is@^16.7.0, react-is@^16.8.1:
|
||||
version "16.8.6"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
|
||||
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
|
||||
|
@ -9615,6 +9627,14 @@ react-overlays@^0.8.3:
|
|||
react-transition-group "^2.2.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-popper@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.3.tgz#33d425fa6975d4bd54d9acd64897a89d904b9d97"
|
||||
integrity sha512-mOEiMNT1249js0jJvkrOjyHsGvqcJd3aGW/agkiMoZk3bZ1fXN1wQszIQSjHIai48fE67+zwF8Cs+C4fWqlfjw==
|
||||
dependencies:
|
||||
react-fast-compare "^3.0.1"
|
||||
warning "^4.0.2"
|
||||
|
||||
react-redux-loading-bar@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/react-redux-loading-bar/-/react-redux-loading-bar-4.5.0.tgz#96538d0ba041463d810e213fb54eadbce9628266"
|
||||
|
@ -9623,17 +9643,16 @@ react-redux-loading-bar@^4.5.0:
|
|||
prop-types "^15.6.2"
|
||||
react-lifecycles-compat "^3.0.2"
|
||||
|
||||
react-redux@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
|
||||
integrity sha512-T52I52Kxhbqy/6TEfBv85rQSDz6+Y28V/pf52vDWs1YRXG19mcFOGfHnY2HsNFHyhP+ST34Aih98fvt6tqwVcQ==
|
||||
react-redux@^7.2.1:
|
||||
version "7.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.1.tgz#8dedf784901014db2feca1ab633864dee68ad985"
|
||||
integrity sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.3.1"
|
||||
"@babel/runtime" "^7.5.5"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
invariant "^2.2.4"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-router-dom@^4.1.1:
|
||||
version "4.3.1"
|
||||
|
@ -9931,10 +9950,10 @@ redux-thunk@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
|
||||
|
||||
redux@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"
|
||||
integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==
|
||||
redux@^4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
symbol-observable "^1.2.0"
|
||||
|
@ -9966,6 +9985,11 @@ regenerator-runtime@^0.13.2:
|
|||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
|
||||
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.7"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
|
||||
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
|
||||
|
||||
regenerator-transform@^0.13.4:
|
||||
version "0.13.4"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb"
|
||||
|
@ -11572,27 +11596,25 @@ ua-parser-js@^0.7.18:
|
|||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
|
||||
integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
|
||||
|
||||
uglify-js@^3.0.0:
|
||||
version "3.4.9"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3"
|
||||
integrity sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==
|
||||
dependencies:
|
||||
commander "~2.17.1"
|
||||
source-map "~0.6.1"
|
||||
uglify-js@^3.6.0:
|
||||
version "3.10.4"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.4.tgz#dd680f5687bc0d7a93b14a3482d16db6eba2bfbb"
|
||||
integrity sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==
|
||||
|
||||
uglifyjs-webpack-plugin@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.1.2.tgz#70e5c38fb2d35ee887949c2a0adb2656c23296d5"
|
||||
integrity sha512-G1fJx2uOAAfvdZ77SVCzmFo6mv8uKaHoZBL9Qq/ciC8r6p0ANOL1uY85fIUiyWXKw5RzAaJYZfNSL58Or2hQ0A==
|
||||
uglifyjs-webpack-plugin@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz#e75bc80e7f1937f725954c9b4c5a1e967ea9d0d7"
|
||||
integrity sha512-mHSkufBmBuJ+KHQhv5H0MXijtsoA1lynJt1lXOaotja8/I0pR4L9oGaPIZw+bQBOFittXZg9OC1sXSGO9D9ZYg==
|
||||
dependencies:
|
||||
cacache "^11.2.0"
|
||||
find-cache-dir "^2.0.0"
|
||||
cacache "^12.0.2"
|
||||
find-cache-dir "^2.1.0"
|
||||
is-wsl "^1.1.0"
|
||||
schema-utils "^1.0.0"
|
||||
serialize-javascript "^1.4.0"
|
||||
serialize-javascript "^1.7.0"
|
||||
source-map "^0.6.1"
|
||||
uglify-js "^3.0.0"
|
||||
webpack-sources "^1.1.0"
|
||||
worker-farm "^1.5.2"
|
||||
uglify-js "^3.6.0"
|
||||
webpack-sources "^1.4.0"
|
||||
worker-farm "^1.7.0"
|
||||
|
||||
unicode-astral-regex@^1.0.1:
|
||||
version "1.0.1"
|
||||
|
@ -11874,6 +11896,13 @@ warning@^4.0.1:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
warning@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
|
||||
|
@ -12139,13 +12168,6 @@ wordwrap@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
|
||||
|
||||
worker-farm@^1.5.2:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
|
||||
integrity sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==
|
||||
dependencies:
|
||||
errno "~0.1.7"
|
||||
|
||||
worker-farm@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
|
||||
|
|
Ładowanie…
Reference in New Issue