Merge remote-tracking branch 'origin' into a11y--

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2021-09-14 14:10:23 +02:00
54 changed files with 701 additions and 262 deletions

View File

@@ -2,6 +2,9 @@ import loadPolyfills from './soapbox/load_polyfills';
require.context('./images/', true);
// Load stylesheet
require('./styles/application.scss');
loadPolyfills().then(() => {
require('./soapbox/main').default();
}).catch(e => {

View File

@@ -471,8 +471,6 @@ export function unsubscribeAccountFail(error) {
export function fetchFollowers(id) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchFollowersRequest(id));
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
@@ -561,8 +559,6 @@ export function expandFollowersFail(id, error) {
export function fetchFollowing(id) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
dispatch(fetchFollowingRequest(id));
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {

View File

@@ -10,6 +10,14 @@ export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_RE
export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL';
export const ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST';
export const ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS';
export const ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL';
export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST';
export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS';
export const ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL = 'ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL';
export function fetchFavouritedStatuses() {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
@@ -96,3 +104,96 @@ export function expandFavouritedStatusesFail(error) {
error,
};
}
export function fetchAccountFavouritedStatuses(accountId) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
if (getState().getIn(['status_lists', `favourites:${accountId}`, 'isLoading'])) {
return;
}
dispatch(fetchAccountFavouritedStatusesRequest(accountId));
api(getState).get(`/api/v1/pleroma/accounts/${accountId}/favourites`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(fetchAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchAccountFavouritedStatusesFail(accountId, error));
});
};
}
export function fetchAccountFavouritedStatusesRequest(accountId) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
accountId,
skipLoading: true,
};
}
export function fetchAccountFavouritedStatusesSuccess(accountId, statuses, next) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
accountId,
statuses,
next,
skipLoading: true,
};
}
export function fetchAccountFavouritedStatusesFail(accountId, error) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
accountId,
error,
skipLoading: true,
};
}
export function expandAccountFavouritedStatuses(accountId) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const url = getState().getIn(['status_lists', `favourites:${accountId}`, 'next'], null);
if (url === null || getState().getIn(['status_lists', `favourites:${accountId}`, 'isLoading'])) {
return;
}
dispatch(expandAccountFavouritedStatusesRequest(accountId));
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
dispatch(expandAccountFavouritedStatusesSuccess(accountId, response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandAccountFavouritedStatusesFail(accountId, error));
});
};
}
export function expandAccountFavouritedStatusesRequest(accountId) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
accountId,
};
}
export function expandAccountFavouritedStatusesSuccess(accountId, statuses, next) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
accountId,
statuses,
next,
};
}
export function expandAccountFavouritedStatusesFail(accountId, error) {
return {
type: ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
accountId,
error,
};
}

View File

@@ -50,6 +50,7 @@ export const makeDefaultConfig = features => {
limit: 1,
}),
aboutPages: ImmutableMap(),
authenticatedProfile: true,
});
};

View File

@@ -11,6 +11,7 @@ const {
BACKEND_URL,
FE_SUBDIRECTORY,
FE_BUILD_DIR,
SENTRY_DSN,
} = process.env;
const sanitizeURL = url => {
@@ -38,4 +39,5 @@ module.exports = sanitize({
BACKEND_URL: sanitizeURL(BACKEND_URL),
FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY),
FE_BUILD_DIR: sanitizePath(FE_BUILD_DIR) || 'static',
SENTRY_DSN,
});

View File

@@ -20,7 +20,7 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
<img
alt="💙"
className="emojione"
src="/emoji/1f499.svg"
src="/packs/emoji/1f499.svg"
/>
:foobar:
</div>

View File

@@ -15,7 +15,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={
Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"👍\\" title=\\":+1:\\" src=\\"/emoji/1f44d.svg\\" />",
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"👍\\" title=\\":+1:\\" src=\\"/packs/emoji/1f44d.svg\\" />",
}
}
onClick={[Function]}
@@ -26,7 +26,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={
Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"❤\\" title=\\":heart:\\" src=\\"/emoji/2764.svg\\" />",
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"❤\\" title=\\":heart:\\" src=\\"/packs/emoji/2764.svg\\" />",
}
}
onClick={[Function]}
@@ -37,7 +37,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={
Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😆\\" title=\\":laughing:\\" src=\\"/emoji/1f606.svg\\" />",
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😆\\" title=\\":laughing:\\" src=\\"/packs/emoji/1f606.svg\\" />",
}
}
onClick={[Function]}
@@ -48,7 +48,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={
Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😮\\" title=\\":open_mouth:\\" src=\\"/emoji/1f62e.svg\\" />",
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😮\\" title=\\":open_mouth:\\" src=\\"/packs/emoji/1f62e.svg\\" />",
}
}
onClick={[Function]}
@@ -59,7 +59,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={
Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😢\\" title=\\":cry:\\" src=\\"/emoji/1f622.svg\\" />",
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😢\\" title=\\":cry:\\" src=\\"/packs/emoji/1f622.svg\\" />",
}
}
onClick={[Function]}
@@ -70,7 +70,7 @@ exports[`<EmojiSelector /> renders correctly 1`] = `
className="emoji-react-selector__emoji"
dangerouslySetInnerHTML={
Object {
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😩\\" title=\\":weary:\\" src=\\"/emoji/1f629.svg\\" />",
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😩\\" title=\\":weary:\\" src=\\"/packs/emoji/1f629.svg\\" />",
}
}
onClick={[Function]}

View File

@@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light';
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
import { joinPublicPath } from 'soapbox/utils/static';
export default class AutosuggestEmoji extends React.PureComponent {
@@ -23,7 +22,7 @@ export default class AutosuggestEmoji extends React.PureComponent {
return null;
}
url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`);
url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
}
return (

View File

@@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import Bowser from 'bowser';
import { captureException } from 'soapbox/monitoring';
export default class ErrorBoundary extends React.PureComponent {
@@ -15,11 +15,21 @@ export default class ErrorBoundary extends React.PureComponent {
}
componentDidCatch(error, info) {
captureException(error);
this.setState({
hasError: true,
error,
componentStack: info && info.componentStack,
});
import(/* webpackChunkName: "error" */'bowser')
.then(({ default: Bowser }) => {
this.setState({
browser: Bowser.getParser(window.navigator.userAgent),
});
})
.catch(() => {});
}
setTextareaRef = c => {
@@ -46,9 +56,7 @@ export default class ErrorBoundary extends React.PureComponent {
}
render() {
const browser = Bowser.getParser(window.navigator.userAgent);
const { hasError } = this.state;
const { browser, hasError } = this.state;
if (!hasError) {
return this.props.children;
@@ -72,9 +80,9 @@ export default class ErrorBoundary extends React.PureComponent {
onClick={this.handleCopy}
readOnly
/>}
<p className='error-boundary__browser'>
{browser && <p className='error-boundary__browser'>
{browser.getBrowserName()} {browser.getBrowserVersion()}
</p>
</p>}
<p className='help-text'>
<FormattedMessage
id='alert.unexpected.help_text'

View File

@@ -265,15 +265,16 @@ class StatusContent extends React.PureComponent {
}
if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} />);
output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
}
return output;
} else {
const output = [
<div
tabIndex='0'
ref={this.setRef}
tabIndex='0'
key='content'
className={classnames('status__content', {
'status__content--big': onlyEmoji,
})}
@@ -284,7 +285,7 @@ class StatusContent extends React.PureComponent {
];
if (status.get('poll')) {
output.push(<PollContainer pollId={status.get('poll')} />);
output.push(<PollContainer pollId={status.get('poll')} key='poll' />);
}
return output;

View File

@@ -17,7 +17,6 @@ import {
isRemote,
getDomain,
} from 'soapbox/utils/accounts';
import { parseVersion } from 'soapbox/utils/features';
import classNames from 'classnames';
import Avatar from 'soapbox/components/avatar';
import { shortNumberFormat } from 'soapbox/utils/numbers';
@@ -30,6 +29,7 @@ import ActionButton from 'soapbox/features/ui/components/action_button';
import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
import { openModal } from 'soapbox/actions/modal';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@@ -72,11 +72,13 @@ const messages = defineMessages({
const mapStateToProps = state => {
const me = state.get('me');
const account = state.getIn(['accounts', me]);
const instance = state.get('instance');
const features = getFeatures(instance);
return {
me,
meAccount: account,
version: parseVersion(state.getIn(['instance', 'version'])),
features,
};
};
@@ -90,7 +92,7 @@ class Header extends ImmutablePureComponent {
identity_props: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
username: PropTypes.string,
version: PropTypes.object,
features: PropTypes.object,
};
state = {
@@ -156,7 +158,7 @@ class Header extends ImmutablePureComponent {
}
makeMenu() {
const { account, intl, me, meAccount, version } = this.props;
const { account, intl, me, meAccount, features } = this.props;
const menu = [];
@@ -196,7 +198,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
// menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
menu.push(null);
} else if (version.software === 'Pleroma') {
} else if (features.unrestrictedLists) {
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
}
@@ -285,7 +287,7 @@ class Header extends ImmutablePureComponent {
}
render() {
const { account, intl, username, me } = this.props;
const { account, intl, username, me, features } = this.props;
const { isSmallScreen } = this.state;
if (!account) {
@@ -327,9 +329,9 @@ class Header extends ImmutablePureComponent {
<StillImage src={account.get('header')} alt='' className='parallax' />
</a>}
<div className='account__header__subscribe'>
{features.accountSubscriptions && <div className='account__header__subscribe'>
<SubscriptionButton account={account} />
</div>
</div>}
</div>
<div className='account__header__bar'>
@@ -356,24 +358,20 @@ class Header extends ImmutablePureComponent {
<span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
</NavLink>}
{
ownAccount &&
<div>
<NavLink
exact activeClassName='active' to={`/@${account.get('acct')}/favorites`}
>
{ /* : TODO : shortNumberFormat(account.get('favourite_count')) */ }
<span></span>
<span><FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' /></span>
</NavLink>
<NavLink
exact activeClassName='active' to={`/@${account.get('acct')}/pins`}
>
{ /* : TODO : shortNumberFormat(account.get('pinned_count')) */ }
<span></span>
<span><FormattedMessage id='navigation_bar.pins' defaultMessage='Pins' /></span>
</NavLink>
</div>
{(ownAccount || !account.getIn(['pleroma', 'hide_favorites'], true)) && <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/favorites`}>
{ /* : TODO : shortNumberFormat(account.get('favourite_count')) */ }
<span></span>
<span><FormattedMessage id='navigation_bar.favourites' defaultMessage='Likes' /></span>
</NavLink>}
{ownAccount &&
<NavLink
exact activeClassName='active' to={`/@${account.get('acct')}/pins`}
>
{ /* : TODO : shortNumberFormat(account.get('pinned_count')) */ }
<span></span>
<span><FormattedMessage id='navigation_bar.pins' defaultMessage='Pins' /></span>
</NavLink>
}
</div>

View File

@@ -7,8 +7,7 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events';
import { buildCustomEmojis } from '../../emoji/emoji';
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
import { joinPublicPath } from 'soapbox/utils/static';
const messages = defineMessages({
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
@@ -29,7 +28,7 @@ const messages = defineMessages({
let EmojiPicker, Emoji; // load asynchronously
const backgroundImageFn = () => join(FE_SUBDIRECTORY, 'emoji', 'sheet_13.png');
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const categoriesSort = [
@@ -358,8 +357,8 @@ class EmojiPickerDropdown extends React.PureComponent {
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
<img
className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂'
src={join(FE_SUBDIRECTORY, 'emoji', '1f602.svg')}
alt='😂'
src={joinPublicPath('packs/emoji/1f602.svg')}
/>
</div>

View File

@@ -4,10 +4,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { setSchedule, removeSchedule } from '../../../actions/compose';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import IconButton from 'soapbox/components/icon_button';
import { removeSchedule } from 'soapbox/actions/compose';
import classNames from 'classnames';
const messages = defineMessages({
@@ -15,11 +15,22 @@ const messages = defineMessages({
remove: { id: 'schedule.remove', defaultMessage: 'Remove schedule' },
});
const mapStateToProps = (state, ownProps) => ({
const mapStateToProps = state => ({
active: state.getIn(['compose', 'schedule']) ? true : false,
scheduledAt: state.getIn(['compose', 'schedule']),
});
export default @connect(mapStateToProps)
const mapDispatchToProps = dispatch => ({
onSchedule(date) {
dispatch(setSchedule(date));
},
onRemoveSchedule(date) {
dispatch(removeSchedule());
},
});
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class ScheduleForm extends React.Component {
@@ -27,6 +38,7 @@ class ScheduleForm extends React.Component {
scheduledAt: PropTypes.instanceOf(Date),
intl: PropTypes.object.isRequired,
onSchedule: PropTypes.func.isRequired,
onRemoveSchedule: PropTypes.func.isRequired,
dispatch: PropTypes.func,
active: PropTypes.bool,
};
@@ -60,7 +72,7 @@ class ScheduleForm extends React.Component {
}
handleRemove = e => {
this.props.dispatch(removeSchedule());
this.props.onRemoveSchedule();
e.preventDefault();
}

View File

@@ -1,16 +1,15 @@
import { connect } from 'react-redux';
import ScheduleForm from '../components/schedule_form';
import { setSchedule } from '../../../actions/compose';
import React from 'react';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { ScheduleForm } from 'soapbox/features/ui/util/async-components';
const mapStateToProps = state => ({
schedule: state.getIn(['compose', 'schedule']),
active: state.getIn(['compose', 'schedule']) ? true : false,
});
export default class ScheduleFormContainer extends React.PureComponent {
const mapDispatchToProps = dispatch => ({
onSchedule(date) {
dispatch(setSchedule(date));
},
});
render() {
return (
<BundleContainer fetchComponent={ScheduleForm}>
{Component => <Component {...this.props} />}
</BundleContainer>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleForm);
}

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon';
import CoinDB from '../utils/coin_db';
import { getCoinIcon } from '../utils/coin_icons';
import CryptoIcon from './crypto_icon';
import { openModal } from 'soapbox/actions/modal';
import { CopyableInput } from 'soapbox/features/forms';
import { getExplorerUrl } from '../utils/block_explorer';
@@ -31,9 +31,11 @@ class CryptoAddress extends ImmutablePureComponent {
return (
<div className='crypto-address'>
<div className='crypto-address__head'>
<div className='crypto-address__icon'>
<img src={getCoinIcon(ticker)} alt={title} />
</div>
<CryptoIcon
className='crypto-address__icon'
ticker={ticker}
title={title}
/>
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
<div className='crypto-address__actions'>
<a href='' onClick={this.handleModalClick}>

View File

@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class CryptoIcon extends React.PureComponent {
static propTypes = {
ticker: PropTypes.string.isRequired,
title: PropTypes.string,
className: PropTypes.string,
}
render() {
const { ticker, title, className } = this.props;
return (
<div className={classNames('crypto-icon', className)}>
<img
src={require(`cryptocurrency-icons/svg/color/${ticker.toLowerCase()}.svg`)}
alt={title || ticker}
/>
</div>
);
}
}

View File

@@ -1,16 +1,14 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon';
import QRCode from 'qrcode.react';
import CoinDB from '../utils/coin_db';
import { getCoinIcon } from '../utils/coin_icons';
import CryptoIcon from './crypto_icon';
import { CopyableInput } from 'soapbox/features/forms';
import { getExplorerUrl } from '../utils/block_explorer';
export default @connect()
class DetailedCryptoAddress extends ImmutablePureComponent {
export default class DetailedCryptoAddress extends ImmutablePureComponent {
static propTypes = {
address: PropTypes.string.isRequired,
@@ -26,9 +24,11 @@ class DetailedCryptoAddress extends ImmutablePureComponent {
return (
<div className='crypto-address'>
<div className='crypto-address__head'>
<div className='crypto-address__icon'>
<img src={getCoinIcon(ticker)} alt={title} />
</div>
<CryptoIcon
className='crypto-address__icon'
ticker={ticker}
title={title}
/>
<div className='crypto-address__title'>{title || ticker.toUpperCase()}</div>
<div className='crypto-address__actions'>
{explorerUrl && <a href={explorerUrl} target='_blank'>

View File

@@ -1,20 +0,0 @@
// Does some trickery to import all the icons into the project
// See: https://stackoverflow.com/questions/42118296/dynamically-import-images-from-a-directory-using-webpack
const icons = {};
function importAll(r) {
const pathRegex = /\.\/(.*)\.svg/i;
r.keys().forEach((key) => {
const ticker = pathRegex.exec(key)[1];
return icons[ticker] = r(key).default;
});
}
importAll(require.context('cryptocurrency-icons/svg/color/', true, /\.svg$/));
export default icons;
// For getting the icon
export const getCoinIcon = ticker => icons[ticker] || icons.generic || null;

View File

@@ -22,23 +22,23 @@ describe('emoji', () => {
it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg" />');
expect(emojify('👨‍👩‍👧‍👧')).toEqual(
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg" />');
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/packs/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg" />');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/packs/emoji/1f469-200d-1f469-200d-1f466.svg" />');
expect(emojify('\u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" />');
});
it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" />');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" />');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" />');
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" />');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg" /> bar');
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/packs/emoji/2757.svg" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/packs/emoji/23-20e3.svg" /> bar');
});
it('ignores unicode inside of tags', () => {
@@ -46,16 +46,16 @@ describe('emoji', () => {
});
it('does multiple emoji properly (issue 5188)', () => {
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />');
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg" />');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/packs/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/packs/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg" />');
});
it('does an emoji that has no shortcode', () => {
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg" />');
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/packs/emoji/1f441-200d-1f5e8.svg" />');
});
it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/packs/emoji/2199.svg" />');
});
it('avoid emojifying on invisible text', () => {
@@ -67,16 +67,16 @@ describe('emoji', () => {
it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/packs/emoji/1f607.svg" />');
});
it('skips the textual presentation VS15 character', () => {
expect(emojify('✴︎')) // This is U+2734 EIGHT POINTED BLACK STAR then U+FE0E VARIATION SELECTOR-15
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/emoji/2734.svg" />');
.toEqual('<img draggable="false" class="emojione" alt="✴" title=":eight_pointed_black_star:" src="/packs/emoji/2734.svg" />');
});
});
});

View File

@@ -1,7 +1,6 @@
import unicodeMapping from './emoji_unicode_mapping_light';
import Trie from 'substring-trie';
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
import { joinPublicPath } from 'soapbox/utils/static';
const trie = new Trie(Object.keys(unicodeMapping));
@@ -62,7 +61,8 @@ const emojify = (str, customEmojis = {}, autoplay = false) => {
} else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : '';
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${join(FE_SUBDIRECTORY, 'emoji', `${filename}.svg`)}" />`;
const src = joinPublicPath(`packs/emoji/${filename}.svg`);
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${src}" />`;
rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) {

View File

@@ -2,23 +2,55 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchFavouritedStatuses, expandFavouritedStatuses } from '../../actions/favourites';
import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from '../../actions/favourites';
import Column from '../ui/components/column';
import StatusList from '../../components/status_list';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash';
import MissingIndicator from 'soapbox/components/missing_indicator';
import { fetchAccount, fetchAccountByUsername } from '../../actions/accounts';
import LoadingIndicator from '../../components/loading_indicator';
const mapStateToProps = (state, { params }) => {
const username = params.username || '';
const me = state.get('me');
const meUsername = state.getIn(['accounts', me, 'username']);
const meUsername = state.getIn(['accounts', me, 'username'], '');
const isMyAccount = (username.toLowerCase() === meUsername.toLowerCase());
if (isMyAccount) {
return {
isMyAccount,
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
};
}
const accounts = state.getIn(['accounts']);
const accountFetchError = (state.getIn(['accounts', -1, 'username'], '').toLowerCase() === username.toLowerCase());
let accountId = -1;
if (accountFetchError) {
accountId = null;
} else {
const account = accounts.find(acct => username.toLowerCase() === acct.getIn(['acct'], '').toLowerCase());
accountId = account ? account.getIn(['id'], null) : -1;
}
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
const unavailable = (me === accountId) ? false : isBlocked;
return {
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
isMyAccount,
accountId,
unavailable,
username,
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['status_lists', `favourites:${accountId}`, 'items'], []),
isLoading: state.getIn(['status_lists', `favourites:${accountId}`, 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', `favourites:${accountId}`, 'next']),
};
};
@@ -36,17 +68,43 @@ class Favourites extends ImmutablePureComponent {
};
componentDidMount() {
this.props.dispatch(fetchFavouritedStatuses());
const { accountId, isMyAccount, username } = this.props;
if (isMyAccount)
this.props.dispatch(fetchFavouritedStatuses());
else {
if (accountId && accountId !== -1) {
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountFavouritedStatuses(accountId));
} else {
this.props.dispatch(fetchAccountByUsername(username));
}
}
}
componentDidUpdate(prevProps) {
const { accountId, isMyAccount } = this.props;
if (!isMyAccount && accountId && accountId !== -1 && (accountId !== prevProps.accountId && accountId)) {
this.props.dispatch(fetchAccount(accountId));
this.props.dispatch(fetchAccountFavouritedStatuses(accountId));
}
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFavouritedStatuses());
const { accountId, isMyAccount } = this.props;
if (isMyAccount) {
this.props.dispatch(expandFavouritedStatuses());
} else {
this.props.dispatch(expandAccountFavouritedStatuses(accountId));
}
}, 300, { leading: true })
render() {
const { statusIds, hasMore, isLoading, isMyAccount } = this.props;
const { statusIds, isLoading, hasMore, isMyAccount, isAccount, accountId, unavailable } = this.props;
if (!isMyAccount) {
if (!isMyAccount && !isAccount && accountId !== -1) {
return (
<Column>
<MissingIndicator />
@@ -54,7 +112,27 @@ class Favourites extends ImmutablePureComponent {
);
}
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any liked posts yet. When you like one, it will show up here." />;
if (accountId === -1) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
if (unavailable) {
return (
<Column>
<div className='empty-column-indicator'>
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
</div>
</Column>
);
}
const emptyMessage = isMyAccount
? <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any liked posts yet. When you like one, it will show up here." />
: <FormattedMessage id='empty_column.account_favourited_statuses' defaultMessage="This user doesn't have any liked posts yet." />;
return (
<Column>

View File

@@ -12,7 +12,7 @@ import MissingIndicator from 'soapbox/components/missing_indicator';
const mapStateToProps = (state, { params }) => {
const username = params.username || '';
const me = state.get('me');
const meUsername = state.getIn(['accounts', me, 'username']);
const meUsername = state.getIn(['accounts', me, 'username'], '');
return {
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
statusIds: state.getIn(['status_lists', 'pins', 'items']),

View File

@@ -73,7 +73,7 @@ class Reactions extends ImmutablePureComponent {
render() {
const { params, reactions, accounts, status } = this.props;
const { username, statusId, reaction } = params;
const { username, statusId } = params;
const back = `/@${username}/posts/${statusId}`;
@@ -95,7 +95,6 @@ class Reactions extends ImmutablePureComponent {
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
console.log(params.reaction);
return (
<Column back={back}>
{

View File

@@ -51,6 +51,8 @@ const messages = defineMessages({
displayFqnLabel: { id: 'soapbox_config.display_fqn_label', defaultMessage: 'Display domain (eg @user@domain) for local accounts.' },
greentextLabel: { id: 'soapbox_config.greentext_label', defaultMessage: 'Enable greentext support' },
promoPanelIconsLink: { id: 'soapbox_config.hints.promo_panel_icons.link', defaultMessage: 'Soapbox Icons List' },
authenticatedProfileLabel: { id: 'soapbox_config.authenticated_profile_label', defaultMessage: 'Profiles require authentication' },
authenticatedProfileHint: { id: 'soapbox_config.authenticated_profile_hint', defaultMessage: 'Users must be logged-in to view replies and media on user profiles.' },
});
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
@@ -279,6 +281,13 @@ class SoapboxConfig extends ImmutablePureComponent {
checked={soapbox.get('greentext') === true}
onChange={this.handleChange(['greentext'], (e) => e.target.checked)}
/>
<Checkbox
name='authenticatedProfile'
label={intl.formatMessage(messages.authenticatedProfileLabel)}
hint={intl.formatMessage(messages.authenticatedProfileHint)}
checked={soapbox.get('authenticatedProfile') === true}
onChange={this.handleChange(['authenticatedProfile'], (e) => e.target.checked)}
/>
</FieldsGroup>
<FieldsGroup>
<div className='input with_block_label popup'>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Immutable from 'immutable';
import { is, fromJS } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
import classnames from 'classnames';
@@ -77,7 +77,7 @@ export default class Card extends React.PureComponent {
};
componentDidUpdate(prevProps) {
if (!Immutable.is(prevProps.card, this.props.card)) {
if (!is(prevProps.card, this.props.card)) {
this.setState({ embedded: false });
}
}
@@ -86,7 +86,7 @@ export default class Card extends React.PureComponent {
const { card, onOpenMedia } = this.props;
onOpenMedia(
Immutable.fromJS([
fromJS([
{
type: 'image',
url: card.get('embed_url'),

View File

@@ -1,4 +1,4 @@
import Immutable from 'immutable';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
@@ -71,11 +71,11 @@ const makeMapStateToProps = () => {
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.OrderedSet();
let ancestorsIds = ImmutableOrderedSet();
let id = statusId;
while (id) {
ancestorsIds = Immutable.OrderedSet([id]).union(ancestorsIds);
ancestorsIds = ImmutableOrderedSet([id]).union(ancestorsIds);
id = inReplyTos.get(id);
}
@@ -86,7 +86,7 @@ const makeMapStateToProps = () => {
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
], (statusId, contextReplies) => {
let descendantsIds = Immutable.OrderedSet();
let descendantsIds = ImmutableOrderedSet();
const ids = [statusId];
while (ids.length > 0) {
@@ -109,8 +109,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
let ancestorsIds = ImmutableOrderedSet();
let descendantsIds = ImmutableOrderedSet();
if (status) {
ancestorsIds = getAncestorsIds(state, { id: state.getIn(['contexts', 'inReplyTos', status.get('id')]) });
@@ -146,8 +146,8 @@ class Status extends ImmutablePureComponent {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
ancestorsIds: ImmutablePropTypes.orderedSet,
descendantsIds: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
domain: PropTypes.string,

View File

@@ -14,13 +14,13 @@ import FocalPointModal from './focal_point_modal';
import HotkeysModal from './hotkeys_modal';
import ComposeModal from './compose_modal';
import UnauthorizedModal from './unauthorized_modal';
import CryptoDonateModal from './crypto_donate_modal';
import EditFederationModal from './edit_federation_modal';
import {
MuteModal,
ReportModal,
EmbedModal,
CryptoDonateModal,
ListEditor,
ListAdder,
} from '../../../features/ui/util/async-components';
@@ -41,7 +41,7 @@ const MODAL_COMPONENTS = {
'HOTKEYS': () => Promise.resolve({ default: HotkeysModal }),
'COMPOSE': () => Promise.resolve({ default: ComposeModal }),
'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }),
'CRYPTO_DONATE': () => Promise.resolve({ default: CryptoDonateModal }),
'CRYPTO_DONATE': CryptoDonateModal,
'EDIT_FEDERATION': () => Promise.resolve({ default: EditFederationModal }),
};

View File

@@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'soapbox/components/icon';
import VerificationBadge from 'soapbox/components/verification_badge';
@@ -13,7 +14,7 @@ import { List as ImmutableList } from 'immutable';
import { getAcct, isAdmin, isModerator, isLocal } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import classNames from 'classnames';
import CryptoAddress from 'soapbox/features/crypto_donate/components/crypto_address';
import { CryptoAddress } from 'soapbox/features/ui/util/async-components';
const TICKER_REGEX = /\$([a-zA-Z]*)/i;
@@ -143,7 +144,15 @@ class ProfileInfoPanel extends ImmutablePureComponent {
{fields.map((pair, i) =>
isTicker(pair.get('name', '')) ? (
<CryptoAddress key={i} ticker={getTicker(pair.get('name')).toLowerCase()} address={pair.get('value_plain')} />
<BundleContainer fetchComponent={CryptoAddress}>
{Component => (
<Component
key={i}
ticker={getTicker(pair.get('name')).toLowerCase()}
address={pair.get('value_plain')}
/>
)}
</BundleContainer>
) : (
<dl className='profile-info-panel-content__fields__item' key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />

View File

@@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { Switch, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
import NotificationsContainer from './containers/notifications_container';
import LoadingBarContainer from './containers/loading_bar_container';
@@ -44,6 +45,7 @@ import ProfileHoverCard from 'soapbox/components/profile_hover_card';
import { getAccessToken } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features';
import { fetchCustomEmojis } from 'soapbox/actions/custom_emojis';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import {
Status,
@@ -121,6 +123,7 @@ const mapStateToProps = state => {
const me = state.get('me');
const account = state.getIn(['accounts', me]);
const instance = state.get('instance');
const soapbox = getSoapboxConfig(state);
return {
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
@@ -129,6 +132,7 @@ const mapStateToProps = state => {
me,
account,
features: getFeatures(instance),
soapbox,
};
};
@@ -166,6 +170,7 @@ class SwitchingColumnsArea extends React.PureComponent {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
soapbox: ImmutablePropTypes.map.isRequired,
};
state = {
@@ -194,7 +199,8 @@ class SwitchingColumnsArea extends React.PureComponent {
}
render() {
const { children } = this.props;
const { children, soapbox } = this.props;
const authenticatedProfile = soapbox.get('authenticatedProfile');
return (
<Switch>
@@ -254,10 +260,10 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path='/mutes' page={DefaultPage} component={Mutes} content={children} />
<WrappedRoute path='/filters' page={DefaultPage} component={Filters} content={children} />
<WrappedRoute path='/@:username' publicRoute exact component={AccountTimeline} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/with_replies' component={AccountTimeline} page={ProfilePage} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/@:username/followers' component={Followers} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/following' component={Following} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/media' component={AccountGallery} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/with_replies' publicRoute={!authenticatedProfile} component={AccountTimeline} page={ProfilePage} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path='/@:username/followers' publicRoute={!authenticatedProfile} component={Followers} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/following' publicRoute={!authenticatedProfile} component={Following} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/media' publicRoute={!authenticatedProfile} component={AccountGallery} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/tagged/:tag' exact component={AccountTimeline} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
@@ -314,6 +320,7 @@ class UI extends React.PureComponent {
streamingUrl: PropTypes.string,
account: PropTypes.object,
features: PropTypes.object.isRequired,
soapbox: ImmutablePropTypes.map.isRequired,
};
state = {
@@ -594,7 +601,7 @@ class UI extends React.PureComponent {
}
render() {
const { streamingUrl, features } = this.props;
const { streamingUrl, features, soapbox } = this.props;
const { draggingOver, mobile } = this.state;
const { intl, children, location, dropdownMenuIsOpen, me } = this.props;
@@ -644,7 +651,7 @@ class UI extends React.PureComponent {
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={classnames} ref={this.setRef} style={style}>
<TabsBar />
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange}>
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} soapbox={soapbox}>
{children}
</SwitchingColumnsArea>

View File

@@ -250,6 +250,18 @@ export function CryptoDonate() {
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate');
}
export function CryptoDonatePanel() {
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate/components/crypto_donate_panel');
}
export function CryptoAddress() {
return import(/* webpackChunkName: "features/crypto_donate" */'../../crypto_donate/components/crypto_address');
}
export function CryptoDonateModal() {
return import(/* webpackChunkName: "features/crypto_donate" */'../components/crypto_donate_modal');
}
export function ScheduledStatuses() {
return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses');
}
@@ -265,3 +277,7 @@ export function FederationRestrictions() {
export function Aliases() {
return import(/* webpackChunkName: "features/aliases" */'../../aliases');
}
export function ScheduleForm() {
return import(/* webpackChunkName: "features/compose" */'../../compose/components/schedule_form');
}

View File

@@ -312,6 +312,7 @@
"emoji_button.search_results": "Wyniki wyszukiwania",
"emoji_button.symbols": "Symbole",
"emoji_button.travel": "Podróże i miejsca",
"empty_column.account_favourited_statuses": "Ten użytkownik nie polubił jeszcze żadnego wpisu.",
"empty_column.account_timeline": "Brak wpisów tutaj!",
"empty_column.account_unavailable": "Profil niedostępny",
"empty_column.aliases": "Nie utworzyłeś(-aś) jeszcze żadnego aliasu konta.",
@@ -321,7 +322,7 @@
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
"empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Kiedy dostaniesz lub wyślesz jakąś, pojawi się ona tutaj.",
"empty_column.domain_blocks": "Brak ukrytych domen.",
"empty_column.favourited_statuses": "Nie dodałeś(-aś) żadnego wpisu do ulubionych. Kiedy to zrobisz, pojawi się on tutaj.",
"empty_column.favourited_statuses": "Nie polubiłeś(-aś) żadnego wpisu. Kiedy to zrobisz, pojawi się on tutaj.",
"empty_column.favourites": "Nikt nie dodał tego wpisu do ulubionych. Gdy ktoś to zrobi, pojawi się tutaj.",
"empty_column.filters": "Nie wyciszyłeś(-aś) jeszcze żadnego słowa.",
"empty_column.follow_requests": "Nie masz żadnych próśb o możliwość śledzenia. Kiedy ktoś utworzy ją, pojawi się tutaj.",

View File

@@ -1,6 +1,5 @@
'use strict';
import './wdyr';
import './precheck';
// FIXME: Push notifications are temporarily removed
// import * as registerPushNotifications from './actions/push_notifications';
@@ -10,12 +9,16 @@ import React from 'react';
import ReactDOM from 'react-dom';
import * as OfflinePluginRuntime from '@lcdp/offline-plugin/runtime';
import * as perf from './performance';
import * as monitoring from './monitoring';
import ready from './ready';
import { NODE_ENV } from 'soapbox/build_config';
function main() {
perf.start('main()');
// Sentry
monitoring.start();
ready(() => {
const mountNode = document.getElementById('soapbox');

View File

@@ -1,8 +1,5 @@
'use strict';
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
const createAudio = sources => {
const audio = new Audio();
sources.forEach(({ type, src }) => {
@@ -31,21 +28,21 @@ export default function soundsMiddleware() {
const soundCache = {
boop: createAudio([
{
src: join(FE_SUBDIRECTORY, '/sounds/boop.ogg'),
src: require('../../sounds/boop.ogg'),
type: 'audio/ogg',
},
{
src: join(FE_SUBDIRECTORY, '/sounds/boop.mp3'),
src: require('../../sounds/boop.mp3'),
type: 'audio/mpeg',
},
]),
chat: createAudio([
{
src: join(FE_SUBDIRECTORY, '/sounds/chat.oga'),
src: require('../../sounds/chat.oga'),
type: 'audio/ogg',
},
{
src: join(FE_SUBDIRECTORY, '/sounds/chat.mp3'),
src: require('../../sounds/chat.mp3'),
type: 'audio/mpeg',
},
]),

27
app/soapbox/monitoring.js Normal file
View File

@@ -0,0 +1,27 @@
import { NODE_ENV, SENTRY_DSN } from 'soapbox/build_config';
export const start = () => {
Promise.all([
import(/* webpackChunkName: "error" */'@sentry/react'),
import(/* webpackChunkName: "error" */'@sentry/tracing'),
]).then(([Sentry, { Integrations: Integrations }]) => {
Sentry.init({
dsn: SENTRY_DSN,
environment: NODE_ENV,
debug: false,
integrations: [new Integrations.BrowserTracing()],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
});
}).catch(console.error);
};
export const captureException = error => {
import(/* webpackChunkName: "error" */'@sentry/react')
.then(Sentry => {
Sentry.captureException(error);
})
.catch(console.error);
};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';
import BundleContainer from '../features/ui/containers/bundle_container';
import ComposeFormContainer from '../features/compose/containers/compose_form_container';
import Avatar from '../components/avatar';
import UserPanel from 'soapbox/features/ui/components/user_panel';
@@ -9,7 +10,7 @@ import WhoToFollowPanel from 'soapbox/features/ui/components/who_to_follow_panel
import TrendsPanel from 'soapbox/features/ui/components/trends_panel';
import PromoPanel from 'soapbox/features/ui/components/promo_panel';
import FundingPanel from 'soapbox/features/ui/components/funding_panel';
import CryptoDonatePanel from 'soapbox/features/crypto_donate/components/crypto_donate_panel';
import { CryptoDonatePanel } from 'soapbox/features/ui/util/async-components';
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
import FeaturesPanel from 'soapbox/features/ui/components/features_panel';
import SignUpPanel from 'soapbox/features/ui/components/sign_up_panel';
@@ -58,7 +59,11 @@ class HomePage extends ImmutablePureComponent {
<div className='columns-area__panels__pane__inner'>
<UserPanel accountId={me} key='user-panel' />
{showFundingPanel && <FundingPanel key='funding-panel' />}
{showCryptoDonatePanel && <CryptoDonatePanel limit={cryptoLimit} key='crypto-panel' />}
{showCryptoDonatePanel && (
<BundleContainer fetchComponent={CryptoDonatePanel}>
{Component => <Component limit={cryptoLimit} key='crypto-panel' />}
</BundleContainer>
)}
</div>
</div>

View File

@@ -1,10 +1,10 @@
import Immutable from 'immutable';
import { Map as ImmutableMap } from 'immutable';
import {
DROPDOWN_MENU_OPEN,
DROPDOWN_MENU_CLOSE,
} from '../actions/dropdown_menu';
const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false });
const initialState = ImmutableMap({ openId: null, placement: null, keyboard: false });
export default function dropdownMenu(state = initialState, action) {
switch (action.type) {

View File

@@ -1,12 +1,12 @@
import Immutable from 'immutable';
import { Map as ImmutableMap } from 'immutable';
import {
MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
} from '../actions/mutes';
const initialState = Immutable.Map({
new: Immutable.Map({
const initialState = ImmutableMap({
new: ImmutableMap({
isSubmitting: false,
account: null,
notifications: true,

View File

@@ -1,9 +1,9 @@
import { SET_BROWSER_SUPPORT, SET_SUBSCRIPTION, CLEAR_SUBSCRIPTION, SET_ALERTS } from '../actions/push_notifications';
import Immutable from 'immutable';
import { Map as ImmutableMap } from 'immutable';
const initialState = Immutable.Map({
const initialState = ImmutableMap({
subscription: null,
alerts: new Immutable.Map({
alerts: new ImmutableMap({
follow: false,
follow_request: false,
favourite: false,
@@ -19,11 +19,11 @@ export default function push_subscriptions(state = initialState, action) {
switch(action.type) {
case SET_SUBSCRIPTION:
return state
.set('subscription', new Immutable.Map({
.set('subscription', new ImmutableMap({
id: action.subscription.id,
endpoint: action.subscription.endpoint,
}))
.set('alerts', new Immutable.Map(action.subscription.alerts))
.set('alerts', new ImmutableMap(action.subscription.alerts))
.set('isSubscribed', true);
case SET_BROWSER_SUPPORT:
return state.set('browserSupport', action.value);

View File

@@ -5,6 +5,12 @@ import {
FAVOURITED_STATUSES_EXPAND_REQUEST,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
FAVOURITED_STATUSES_EXPAND_FAIL,
ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS,
ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL,
} from '../actions/favourites';
import {
BOOKMARKED_STATUSES_FETCH_REQUEST,
@@ -101,6 +107,16 @@ export default function statusLists(state = initialState, action) {
return normalizeList(state, 'favourites', action.statuses, action.next);
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, 'favourites', action.statuses, action.next);
case ACCOUNT_FAVOURITED_STATUSES_FETCH_REQUEST:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_REQUEST:
return setLoading(state, `favourites:${action.accountId}`, true);
case ACCOUNT_FAVOURITED_STATUSES_FETCH_FAIL:
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_FAIL:
return setLoading(state, `favourites:${action.accountId}`, false);
case ACCOUNT_FAVOURITED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, `favourites:${action.accountId}`, action.statuses, action.next);
case ACCOUNT_FAVOURITED_STATUSES_EXPAND_SUCCESS:
return appendToList(state, `favourites:${action.accountId}`, action.statuses, action.next);
case BOOKMARKED_STATUSES_FETCH_REQUEST:
case BOOKMARKED_STATUSES_EXPAND_REQUEST:
return setLoading(state, 'bookmarks', true);

View File

@@ -26,6 +26,8 @@ export const getFeatures = createSelector([
accountAliasesAPI: v.software === 'Pleroma',
resetPasswordAPI: v.software === 'Pleroma',
exposableReactions: features.includes('exposable_reactions'),
accountSubscriptions: v.software === 'Pleroma' && gte(v.version, '1.0.0'),
unrestrictedLists: v.software === 'Pleroma',
};
});

View File

@@ -1,6 +1,4 @@
/* eslint-disable no-case-declarations */
import EXIF from 'exif-js';
const MAX_IMAGE_PIXELS = 2073600; // 1920x1080px
const _browser_quirks = {};
@@ -115,14 +113,16 @@ const getOrientation = (img, type = 'image/png') => new Promise(resolve => {
return;
}
EXIF.getData(img, () => {
const orientation = EXIF.getTag(img, 'Orientation');
if (orientation !== 1) {
dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation));
} else {
resolve(orientation);
}
});
import(/* webpackChunkName: "features/compose" */'exif-js').then(({ default: EXIF }) => {
EXIF.getData(img, () => {
const orientation = EXIF.getTag(img, 'Orientation');
if (orientation !== 1) {
dropOrientationIfNeeded(orientation).then(resolve).catch(() => resolve(orientation));
} else {
resolve(orientation);
}
});
}).catch(() => {});
});
const processImage = (img, { width, height, orientation, type = 'image/png', name = 'resized.png' }) => new Promise(resolve => {

View File

@@ -0,0 +1,11 @@
/**
* Static: functions related to static files.
* @module soapbox/utils/static
*/
import { join } from 'path';
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
export const joinPublicPath = (...paths) => {
return join(FE_SUBDIRECTORY, ...paths);
};

View File

@@ -1,7 +0,0 @@
import React from 'react';
import { NODE_ENV } from 'soapbox/build_config';
if (NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React);
}

View File

@@ -156,6 +156,7 @@
display: flex !important;
align-items: center !important;
transition: 0.2s !important;
background: var(--foreground-color);
&:hover {
background-color: var(--background-color) !important;