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

@@ -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');
}