Merge branch 'develop' into 'public-favourites'
# Conflicts: # app/soapbox/features/favourited_statuses/index.js
This commit is contained in:
commit
495882658a
38 changed files with 434 additions and 164 deletions
|
@ -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 => {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -28,6 +28,10 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
export const REACTIONS_FETCH_REQUEST = 'REACTIONS_FETCH_REQUEST';
|
||||
export const REACTIONS_FETCH_SUCCESS = 'REACTIONS_FETCH_SUCCESS';
|
||||
export const REACTIONS_FETCH_FAIL = 'REACTIONS_FETCH_FAIL';
|
||||
|
||||
export const PIN_REQUEST = 'PIN_REQUEST';
|
||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||
export const PIN_FAIL = 'PIN_FAIL';
|
||||
|
@ -359,6 +363,41 @@ export function fetchFavouritesFail(id, error) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchReactions(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchReactionsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/pleroma/statuses/${id}/reactions`).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(({ accounts }) => accounts).flat()));
|
||||
dispatch(fetchReactionsSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReactionsFail(id, error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsRequest(id) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsSuccess(id, reactions) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_SUCCESS,
|
||||
id,
|
||||
reactions,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReactionsFail(id, error) {
|
||||
return {
|
||||
type: REACTIONS_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function pin(status) {
|
||||
return (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
|
|
@ -50,6 +50,7 @@ export const makeDefaultConfig = features => {
|
|||
limit: 1,
|
||||
}),
|
||||
aboutPages: ImmutableMap(),
|
||||
authenticatedProfile: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,11 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
|
|||
<img
|
||||
alt="💙"
|
||||
className="emojione"
|
||||
src="/emoji/1f499.svg"
|
||||
src={
|
||||
Object {
|
||||
"process": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
:foobar:
|
||||
</div>
|
||||
|
|
|
@ -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=\\"[object Object]\\" />",
|
||||
}
|
||||
}
|
||||
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=\\"[object Object]\\" />",
|
||||
}
|
||||
}
|
||||
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=\\"[object Object]\\" />",
|
||||
}
|
||||
}
|
||||
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=\\"[object Object]\\" />",
|
||||
}
|
||||
}
|
||||
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=\\"[object Object]\\" />",
|
||||
}
|
||||
}
|
||||
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=\\"[object Object]\\" />",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
|
|
|
@ -12,6 +12,7 @@ import RelativeTimestamp from './relative_timestamp';
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
|
@ -46,6 +47,7 @@ class Account extends ImmutablePureComponent {
|
|||
onActionClick: PropTypes.func,
|
||||
withDate: PropTypes.bool,
|
||||
withRelationship: PropTypes.bool,
|
||||
reaction: PropTypes.string,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -78,7 +80,7 @@ class Account extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, me, withDate, withRelationship } = this.props;
|
||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, me, withDate, withRelationship, reaction } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <div />;
|
||||
|
@ -95,6 +97,7 @@ class Account extends ImmutablePureComponent {
|
|||
|
||||
let buttons;
|
||||
let followedBy;
|
||||
let emoji;
|
||||
|
||||
if (onActionClick && actionIcon) {
|
||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||
|
@ -128,6 +131,15 @@ class Account extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (reaction) {
|
||||
emoji = (
|
||||
<span
|
||||
className='emoji-react__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(reaction) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const createdAt = account.get('created_at');
|
||||
|
||||
const joinedAt = createdAt ? (
|
||||
|
@ -141,7 +153,10 @@ class Account extends ImmutablePureComponent {
|
|||
<div className={classNames('account', { 'account--with-relationship': withRelationship, 'account--with-date': withDate })}>
|
||||
<div className='account__wrapper'>
|
||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={`/@${account.get('acct')}`} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<div className='account__avatar-wrapper'>
|
||||
{emoji}
|
||||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
<DisplayName account={account} withDate={Boolean(withDate && withRelationship)} />
|
||||
</Permalink>
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
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';
|
||||
|
||||
export default class AutosuggestEmoji extends React.PureComponent {
|
||||
|
||||
|
@ -23,7 +21,7 @@ export default class AutosuggestEmoji extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
url = join(FE_SUBDIRECTORY, 'emoji', `${mapping.filename}.svg`);
|
||||
url = require(`twemoji/assets/svg/${mapping.filename}.svg`);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,13 +5,19 @@ import Icon from 'soapbox/components/icon';
|
|||
|
||||
export default class ColumnBackButton extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
to: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
const { to } = this.props;
|
||||
|
||||
if (window.history && window.history.length === 1) {
|
||||
this.context.router.history.push('/');
|
||||
this.context.router.history.push(to ? to : '/');
|
||||
} else {
|
||||
this.context.router.history.goBack();
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ const makeMapStateToProps = () => {
|
|||
unavailable,
|
||||
accountUsername,
|
||||
accountApId,
|
||||
isBlocked,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
statusIds: getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' }),
|
||||
featuredStatusIds: showPins ? getStatusIds(state, { type: `account:${accountId}:pinned`, prefix: 'account_timeline' }) : ImmutableOrderedSet(),
|
||||
|
@ -142,7 +143,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { statusIds, featuredStatusIds, isLoading, hasMore, isBlocked, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { collapsed, animating } = this.state;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
|
@ -165,7 +166,8 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
{isBlocked ? <FormattedMessage id='empty_column.account_blocked' defaultMessage='You are blocked by @{accountUsername}.' values={{ accountUsername: accountUsername }} />
|
||||
: <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -7,8 +7,6 @@ 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';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
|
@ -29,7 +27,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 +356,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={require('twemoji/assets/svg/1f602.svg')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,20 +1,4 @@
|
|||
// 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;
|
||||
export const getCoinIcon = ticker => {
|
||||
return require(`cryptocurrency-icons/svg/color/${ticker.toLowerCase()}.svg`);
|
||||
};
|
||||
|
|
|
@ -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="[object Object]" />');
|
||||
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="[object Object]" />');
|
||||
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="[object Object]" />');
|
||||
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="[object Object]" />');
|
||||
});
|
||||
|
||||
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="[object Object]" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="[object Object]" />');
|
||||
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="[object Object]" /><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="[object Object]" />');
|
||||
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="[object Object]" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="[object Object]" /> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="[object Object]" />');
|
||||
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="[object Object]" /> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="[object Object]" /> 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="[object Object]" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="[object Object]" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="[object Object]" />');
|
||||
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="[object Object]" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="[object Object]" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="[object Object]" />');
|
||||
});
|
||||
|
||||
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="[object Object]" />');
|
||||
});
|
||||
|
||||
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="[object Object]" />');
|
||||
});
|
||||
|
||||
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="[object Object]" />');
|
||||
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="[object Object]" />');
|
||||
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="[object Object]" />');
|
||||
});
|
||||
|
||||
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="[object Object]" />');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import unicodeMapping from './emoji_unicode_mapping_light';
|
||||
import Trie from 'substring-trie';
|
||||
import { join } from 'path';
|
||||
import { FE_SUBDIRECTORY } from 'soapbox/build_config';
|
||||
|
||||
const trie = new Trie(Object.keys(unicodeMapping));
|
||||
|
||||
|
@ -62,7 +60,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 = require(`twemoji/assets/svg/${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) {
|
||||
|
|
|
@ -15,7 +15,7 @@ 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());
|
||||
|
||||
|
|
|
@ -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']),
|
||||
|
|
120
app/soapbox/features/reactions/index.js
Normal file
120
app/soapbox/features/reactions/index.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import LoadingIndicator from '../../components/loading_indicator';
|
||||
import MissingIndicator from '../../components/missing_indicator';
|
||||
import { fetchFavourites, fetchReactions } from '../../actions/interactions';
|
||||
import { fetchStatus } from '../../actions/statuses';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import AccountContainer from '../../containers/account_container';
|
||||
import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { makeGetStatus } from '../../selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const getStatus = makeGetStatus();
|
||||
const status = getStatus(state, {
|
||||
id: props.params.statusId,
|
||||
username: props.params.username,
|
||||
});
|
||||
|
||||
const favourites = state.getIn(['user_lists', 'favourited_by', props.params.statusId]);
|
||||
const reactions = state.getIn(['user_lists', 'reactions', props.params.statusId]);
|
||||
const allReactions = favourites && reactions && ImmutableOrderedSet(favourites ? [{ accounts: favourites, count: favourites.size, name: '👍' }] : []).union(reactions || []);
|
||||
|
||||
return {
|
||||
status,
|
||||
reactions: allReactions,
|
||||
accounts: allReactions && (props.params.reaction
|
||||
? allReactions.find(reaction => reaction.name === props.params.reaction).accounts.map(account => ({ id: account, reaction: props.params.reaction }))
|
||||
: allReactions.map(reaction => reaction.accounts.map(account => ({ id: account, reaction: reaction.name }))).flatten()),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Reactions extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
reactions: ImmutablePropTypes.orderedSet,
|
||||
accounts: ImmutablePropTypes.orderedSet,
|
||||
status: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
this.props.dispatch(fetchReactions(this.props.params.statusId));
|
||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { params } = this.props;
|
||||
if (params.statusId !== prevProps.params.statusId && params.statusId) {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
prevProps.dispatch(fetchReactions(params.statusId));
|
||||
prevProps.dispatch(fetchStatus(params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
handleFilterChange = (reaction) => () => {
|
||||
const { params } = this.props;
|
||||
const { username, statusId } = params;
|
||||
|
||||
this.context.router.history.replace(`/@${username}/posts/${statusId}/reactions/${reaction}`);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { params, reactions, accounts, status } = this.props;
|
||||
const { username, statusId } = params;
|
||||
|
||||
const back = `/@${username}/posts/${statusId}`;
|
||||
|
||||
if (!accounts) {
|
||||
return (
|
||||
<Column back={back}>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<Column back={back}>
|
||||
<MissingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column back={back}>
|
||||
{
|
||||
reactions.size > 0 && (
|
||||
<div className='reaction__filter-bar'>
|
||||
<button onClick={this.handleFilterChange('')}>All</button>
|
||||
{reactions?.filter(reaction => reaction.count).map(reaction => <button key={reaction.name} onClick={this.handleFilterChange(reaction.name)}>{reaction.name} {reaction.count}</button>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ScrollableList
|
||||
scrollKey='reactions'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accounts.map((account) =>
|
||||
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} withNote={false} reaction={account.reaction} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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'>
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import { FormattedNumber } from 'react-intl';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { reduceEmoji } from 'soapbox/utils/emoji_reacts';
|
||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
|
||||
return {
|
||||
allowedEmoji: getSoapboxConfig(state).get('allowedEmoji'),
|
||||
reactionList: features.exposableReactions,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class StatusInteractionBar extends ImmutablePureComponent {
|
||||
|
@ -21,6 +29,7 @@ class StatusInteractionBar extends ImmutablePureComponent {
|
|||
status: ImmutablePropTypes.map,
|
||||
me: SoapboxPropTypes.me,
|
||||
allowedEmoji: ImmutablePropTypes.list,
|
||||
reactionList: PropTypes.bool,
|
||||
}
|
||||
|
||||
getNormalizedReacts = () => {
|
||||
|
@ -49,35 +58,53 @@ class StatusInteractionBar extends ImmutablePureComponent {
|
|||
return '';
|
||||
}
|
||||
|
||||
render() {
|
||||
getEmojiReacts = () => {
|
||||
const { status, reactionList } = this.props;
|
||||
|
||||
const emojiReacts = this.getNormalizedReacts();
|
||||
const count = emojiReacts.reduce((acc, cur) => (
|
||||
acc + cur.get('count')
|
||||
), 0);
|
||||
const repost = this.getRepost();
|
||||
|
||||
const EmojiReactsContainer = () => (
|
||||
<div className='emoji-reacts-container'>
|
||||
<div className='emoji-reacts'>
|
||||
{emojiReacts.map((e, i) => (
|
||||
<span className='emoji-react' key={i}>
|
||||
<span
|
||||
className='emoji-react__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(e.get('name')) }}
|
||||
/>
|
||||
<span className='emoji-react__count'>{e.get('count')}</span>
|
||||
</span>
|
||||
))}
|
||||
if (count > 0) {
|
||||
return (
|
||||
<div className='emoji-reacts-container'>
|
||||
<div className='emoji-reacts'>
|
||||
{emojiReacts.map((e, i) => {
|
||||
const emojiReact = (
|
||||
<>
|
||||
<span
|
||||
className='emoji-react__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(e.get('name')) }}
|
||||
/>
|
||||
<span className='emoji-react__count'>{e.get('count')}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (reactionList) {
|
||||
return <Link to={`/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}/reactions/${e.get('name')}`} className='emoji-react' key={i}>{emojiReact}</Link>;
|
||||
}
|
||||
|
||||
return <span className='emoji-react' key={i}>{emojiReact}</span>;
|
||||
})}
|
||||
</div>
|
||||
<div className='emoji-reacts__count'>
|
||||
{count}
|
||||
</div>
|
||||
</div>
|
||||
<div className='emoji-reacts__count'>
|
||||
{count}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
render() {
|
||||
const emojiReacts = this.getEmojiReacts();
|
||||
const repost = this.getRepost();
|
||||
|
||||
return (
|
||||
<div className='status-interaction-bar'>
|
||||
{count > 0 && <EmojiReactsContainer />}
|
||||
{emojiReacts}
|
||||
{repost}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,12 +12,13 @@ export default class Column extends React.PureComponent {
|
|||
children: PropTypes.node,
|
||||
active: PropTypes.bool,
|
||||
backBtnSlim: PropTypes.bool,
|
||||
back: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { heading, icon, children, active, backBtnSlim } = this.props;
|
||||
const { heading, icon, children, active, backBtnSlim, back } = this.props;
|
||||
const columnHeaderId = heading && heading.replace(/ /g, '-');
|
||||
const backBtn = backBtnSlim ? (<ColumnBackButtonSlim />) : (<ColumnBackButton />);
|
||||
const backBtn = backBtnSlim ? (<ColumnBackButtonSlim to={back} />) : (<ColumnBackButton to={back} />);
|
||||
|
||||
return (
|
||||
<div role='region' aria-labelledby={columnHeaderId} className='column'>
|
||||
|
|
|
@ -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,
|
||||
|
@ -56,6 +58,7 @@ import {
|
|||
Followers,
|
||||
Following,
|
||||
Reblogs,
|
||||
Reactions,
|
||||
// Favourites,
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
|
@ -120,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,
|
||||
|
@ -128,6 +132,7 @@ const mapStateToProps = state => {
|
|||
me,
|
||||
account,
|
||||
features: getFeatures(instance),
|
||||
soapbox,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -165,6 +170,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
children: PropTypes.node,
|
||||
location: PropTypes.object,
|
||||
onLayoutChange: PropTypes.func.isRequired,
|
||||
soapbox: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -193,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>
|
||||
|
@ -253,15 +260,16 @@ 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} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={DefaultPage} component={Status} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/reblogs' page={DefaultPage} component={Reblogs} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/reactions/:reaction?' page={DefaultPage} component={Reactions} content={children} />
|
||||
|
||||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
<WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />
|
||||
|
@ -312,6 +320,7 @@ class UI extends React.PureComponent {
|
|||
streamingUrl: PropTypes.string,
|
||||
account: PropTypes.object,
|
||||
features: PropTypes.object.isRequired,
|
||||
soapbox: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -592,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;
|
||||
|
||||
|
@ -642,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>
|
||||
|
||||
|
|
|
@ -94,6 +94,10 @@ export function Reblogs() {
|
|||
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
|
||||
}
|
||||
|
||||
export function Reactions() {
|
||||
return import(/* webpackChunkName: "features/reactions" */'../../reactions');
|
||||
}
|
||||
|
||||
export function Favourites() {
|
||||
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]),
|
||||
|
|
|
@ -10,6 +10,7 @@ describe('user_lists reducer', () => {
|
|||
favourited_by: ImmutableMap(),
|
||||
follow_requests: ImmutableMap(),
|
||||
blocks: ImmutableMap(),
|
||||
reactions: ImmutableMap(),
|
||||
mutes: ImmutableMap(),
|
||||
groups: ImmutableMap(),
|
||||
groups_removed_accounts: ImmutableMap(),
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
import {
|
||||
REBLOGS_FETCH_SUCCESS,
|
||||
FAVOURITES_FETCH_SUCCESS,
|
||||
REACTIONS_FETCH_SUCCESS,
|
||||
} from '../actions/interactions';
|
||||
import {
|
||||
BLOCKS_FETCH_SUCCESS,
|
||||
|
@ -37,6 +38,7 @@ const initialState = ImmutableMap({
|
|||
following: ImmutableMap(),
|
||||
reblogged_by: ImmutableMap(),
|
||||
favourited_by: ImmutableMap(),
|
||||
reactions: ImmutableMap(),
|
||||
follow_requests: ImmutableMap(),
|
||||
blocks: ImmutableMap(),
|
||||
mutes: ImmutableMap(),
|
||||
|
@ -77,6 +79,8 @@ export default function userLists(state = initialState, action) {
|
|||
return state.setIn(['reblogged_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
|
||||
case FAVOURITES_FETCH_SUCCESS:
|
||||
return state.setIn(['favourited_by', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
|
||||
case REACTIONS_FETCH_SUCCESS:
|
||||
return state.setIn(['reactions', action.id], action.reactions.map(({ accounts, ...reaction }) => ({ ...reaction, accounts: ImmutableOrderedSet(accounts.map(account => account.id)) })));
|
||||
case NOTIFICATIONS_UPDATE:
|
||||
return action.notification.type === 'follow_request' ? normalizeFollowRequest(state, action.notification) : state;
|
||||
case FOLLOW_REQUESTS_FETCH_SUCCESS:
|
||||
|
|
|
@ -25,6 +25,7 @@ export const getFeatures = createSelector([
|
|||
settingsStore: v.software === 'Pleroma',
|
||||
accountAliasesAPI: v.software === 'Pleroma',
|
||||
resetPasswordAPI: v.software === 'Pleroma',
|
||||
exposableReactions: features.includes('exposable_reactions'),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -216,6 +216,13 @@
|
|||
.account__avatar-wrapper {
|
||||
float: left;
|
||||
margin-right: 12px;
|
||||
|
||||
.emoji-react__emoji {
|
||||
position: absolute;
|
||||
top: 36px;
|
||||
left: 32px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.emoji-react {
|
||||
display: inline-block;
|
||||
transition: 0.1s;
|
||||
color: var(--primary-text-color--faint);
|
||||
text-decoration: none;
|
||||
|
||||
&__emoji {
|
||||
img {
|
||||
|
@ -20,8 +22,6 @@
|
|||
}
|
||||
|
||||
.emoji-react--reblogs {
|
||||
color: var(--primary-text-color--faint);
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
|
||||
|
|
|
@ -613,7 +613,8 @@
|
|||
|
||||
.notification__filter-bar,
|
||||
.search__filter-bar,
|
||||
.account__section-headline {
|
||||
.account__section-headline,
|
||||
.reaction__filter-bar {
|
||||
border-bottom: 1px solid var(--brand-color--faint);
|
||||
cursor: default;
|
||||
display: flex;
|
||||
|
@ -663,6 +664,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.reaction__filter-bar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
a {
|
||||
flex: unset;
|
||||
padding: 15px 24px;
|
||||
min-width: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
|
|
@ -31,4 +31,11 @@ module.exports = {
|
|||
'<rootDir>/app',
|
||||
],
|
||||
'testEnvironment': 'jsdom',
|
||||
'moduleNameMapper': {
|
||||
'^.+.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub',
|
||||
},
|
||||
'transform': {
|
||||
'\\.[jt]sx?$': 'babel-jest',
|
||||
'.+\\.(css|styl|less|sass|scss|png|jpg|svg|ttf|woff|woff2)$': 'jest-transform-stub',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
"intl-messageformat-parser": "^6.0.0",
|
||||
"intl-pluralrules": "^1.3.0",
|
||||
"is-nan": "^1.2.1",
|
||||
"jest-transform-stub": "^2.0.0",
|
||||
"jsdoc": "~3.6.7",
|
||||
"lodash": "^4.7.11",
|
||||
"mark-loader": "^0.1.6",
|
||||
|
|
|
@ -12,7 +12,6 @@ const settings = {
|
|||
test_root_path: `${FE_BUILD_DIR}-test`,
|
||||
cache_path: 'tmp/cache',
|
||||
resolved_paths: [],
|
||||
static_assets_extensions: [ '.jpg', '.jpeg', '.png', '.tiff', '.ico', '.svg', '.gif', '.eot', '.otf', '.ttf', '.woff', '.woff2', '.mp3', '.ogg', '.oga' ],
|
||||
extensions: [ '.mjs', '.js', '.sass', '.scss', '.css', '.module.sass', '.module.scss', '.module.css', '.png', '.svg', '.gif', '.jpeg', '.jpg' ],
|
||||
};
|
||||
|
||||
|
|
|
@ -25,37 +25,33 @@ module.exports = merge(sharedConfig, {
|
|||
new OfflinePlugin({
|
||||
caches: {
|
||||
main: [':rest:'],
|
||||
additional: [':externals:'],
|
||||
additional: [
|
||||
'packs/emoji/1f602-*.svg', // used for emoji picker dropdown
|
||||
'packs/images/32-*.png', // used in emoji-mart
|
||||
|
||||
// Default emoji reacts
|
||||
'packs/emoji/1f44d-*.svg', // Thumbs up
|
||||
'packs/emoji/2764-*.svg', // Heart
|
||||
'packs/emoji/1f606-*.svg', // Laughing
|
||||
'packs/emoji/1f62e-*.svg', // Surprised
|
||||
'packs/emoji/1f622-*.svg', // Crying
|
||||
'packs/emoji/1f629-*.svg', // Weary
|
||||
'packs/emoji/1f621-*.svg', // Angry (Spinster)
|
||||
],
|
||||
optional: [
|
||||
'**/locale_*.js', // don't fetch every locale; the user only needs one
|
||||
'**/*_polyfills-*.js', // the user may not need polyfills
|
||||
'**/*.chunk.js', // only cache chunks when needed
|
||||
'**/*.woff2', // the user may have system-fonts enabled
|
||||
// images/audio can be cached on-demand
|
||||
// images can be cached on-demand
|
||||
'**/*.png',
|
||||
'**/*.jpg',
|
||||
'**/*.jpeg',
|
||||
'**/*.svg',
|
||||
'**/*.mp3',
|
||||
'**/*.ogg',
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
'/emoji/1f602.svg', // used for emoji picker dropdown
|
||||
'/emoji/sheet_13.png', // used in emoji-mart
|
||||
|
||||
// Default emoji reacts
|
||||
'/emoji/1f44d.svg', // Thumbs up
|
||||
'/emoji/2764.svg', // Heart
|
||||
'/emoji/1f606.svg', // Laughing
|
||||
'/emoji/1f62e.svg', // Surprised
|
||||
'/emoji/1f622.svg', // Crying
|
||||
'/emoji/1f629.svg', // Weary
|
||||
'/emoji/1f621.svg', // Angry (Spinster)
|
||||
],
|
||||
excludes: [
|
||||
'**/*.gz',
|
||||
'**/*.map',
|
||||
'**/*.LICENSE.txt',
|
||||
'stats.json',
|
||||
'report.html',
|
||||
'instance/**/*',
|
||||
|
@ -66,15 +62,20 @@ module.exports = merge(sharedConfig, {
|
|||
'**/*.woff',
|
||||
// Sounds return a 206 causing sw.js to crash
|
||||
// https://stackoverflow.com/a/66335638
|
||||
'sounds/**/*',
|
||||
// Don't cache index.html
|
||||
'**/*.ogg',
|
||||
'**/*.oga',
|
||||
'**/*.mp3',
|
||||
// Don't serve index.html
|
||||
// https://github.com/bromite/bromite/issues/1294
|
||||
'index.html',
|
||||
'404.html',
|
||||
],
|
||||
// ServiceWorker: {
|
||||
// entry: join(__dirname, '../app/soapbox/service_worker/entry.js'),
|
||||
// cacheName: 'soapbox',
|
||||
// minify: true,
|
||||
// },
|
||||
safeToUseOptionalCaches: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
50
webpack/rules/assets.js
Normal file
50
webpack/rules/assets.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Asset modules
|
||||
// https://webpack.js.org/guides/asset-modules/
|
||||
|
||||
const { resolve } = require('path');
|
||||
|
||||
// These are processed in reverse-order
|
||||
// We use the name 'packs' instead of 'assets' for legacy reasons
|
||||
module.exports = [{
|
||||
test: /\.(png|svg)/,
|
||||
type: 'asset/resource',
|
||||
include: [
|
||||
resolve('app', 'images'),
|
||||
resolve('node_modules', 'emoji-datasource'),
|
||||
],
|
||||
generator: {
|
||||
filename: 'packs/images/[name]-[contenthash:8][ext]',
|
||||
},
|
||||
}, {
|
||||
test: /\.(ttf|eot|svg|woff|woff2)/,
|
||||
type: 'asset/resource',
|
||||
include: [
|
||||
resolve('app', 'fonts'),
|
||||
resolve('node_modules', 'fork-awesome'),
|
||||
resolve('node_modules', '@fontsource'),
|
||||
],
|
||||
generator: {
|
||||
filename: 'packs/fonts/[name]-[contenthash:8][ext]',
|
||||
},
|
||||
}, {
|
||||
test: /\.(ogg|oga|mp3)/,
|
||||
type: 'asset/resource',
|
||||
include: resolve('app', 'sounds'),
|
||||
generator: {
|
||||
filename: 'packs/sounds/[name]-[contenthash:8][ext]',
|
||||
},
|
||||
}, {
|
||||
test: /\.svg$/,
|
||||
type: 'asset/resource',
|
||||
include: resolve('node_modules', 'twemoji'),
|
||||
generator: {
|
||||
filename: 'packs/emoji/[name]-[contenthash:8][ext]',
|
||||
},
|
||||
}, {
|
||||
test: /\.svg$/,
|
||||
type: 'asset/resource',
|
||||
include: resolve('node_modules', 'cryptocurrency-icons'),
|
||||
generator: {
|
||||
filename: 'packs/images/crypto/[name]-[contenthash:8][ext]',
|
||||
},
|
||||
}];
|
|
@ -1,20 +0,0 @@
|
|||
const { join } = require('path');
|
||||
const { settings } = require('../configuration');
|
||||
|
||||
module.exports = {
|
||||
test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'),
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name(file) {
|
||||
if (file.includes(settings.source_path)) {
|
||||
return 'packs/media/[path][name]-[contenthash].[ext]';
|
||||
}
|
||||
return 'packs/media/[folder]/[name]-[contenthash:8].[ext]';
|
||||
},
|
||||
context: join(settings.source_path),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -3,14 +3,14 @@ const git = require('./babel-git');
|
|||
const gitRefresh = require('./git-refresh');
|
||||
const buildConfig = require('./babel-build-config');
|
||||
const css = require('./css');
|
||||
const file = require('./file');
|
||||
const assets = require('./assets');
|
||||
const nodeModules = require('./node_modules');
|
||||
|
||||
// Webpack loaders are processed in reverse order
|
||||
// https://webpack.js.org/concepts/loaders/#loader-features
|
||||
// Lastly, process static files using file loader
|
||||
module.exports = [
|
||||
file,
|
||||
...assets,
|
||||
css,
|
||||
nodeModules,
|
||||
babel,
|
||||
|
|
|
@ -30,10 +30,9 @@ const makeHtmlConfig = (params = {}) => {
|
|||
};
|
||||
|
||||
module.exports = {
|
||||
entry: Object.assign(
|
||||
{ application: resolve('app/application.js') },
|
||||
{ styles: resolve(join(settings.source_path, 'styles/application.scss')) },
|
||||
),
|
||||
entry: {
|
||||
application: resolve('app/application.js'),
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: 'packs/js/[name]-[chunkhash].js',
|
||||
|
@ -65,7 +64,7 @@ module.exports = {
|
|||
},
|
||||
|
||||
module: {
|
||||
rules: Object.keys(rules).map(key => rules[key]),
|
||||
rules,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
|
@ -89,15 +88,6 @@ module.exports = {
|
|||
new HtmlWebpackHarddiskPlugin(),
|
||||
new CopyPlugin({
|
||||
patterns: [{
|
||||
from: join(__dirname, '../node_modules/twemoji/assets/svg'),
|
||||
to: join(output.path, 'emoji'),
|
||||
}, {
|
||||
from: join(__dirname, '../node_modules/emoji-datasource/img/twitter/sheets/32.png'),
|
||||
to: join(output.path, 'emoji/sheet_13.png'),
|
||||
}, {
|
||||
from: join(__dirname, '../app/sounds'),
|
||||
to: join(output.path, 'sounds'),
|
||||
}, {
|
||||
from: join(__dirname, '../app/instance'),
|
||||
to: join(output.path, 'instance'),
|
||||
}],
|
||||
|
|
|
@ -7239,6 +7239,11 @@ jest-snapshot@^27.1.0:
|
|||
pretty-format "^27.1.0"
|
||||
semver "^7.3.2"
|
||||
|
||||
jest-transform-stub@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz#19018b0851f7568972147a5d60074b55f0225a7d"
|
||||
integrity sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==
|
||||
|
||||
jest-util@^27.0.0:
|
||||
version "27.0.6"
|
||||
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-27.0.6.tgz#e8e04eec159de2f4d5f57f795df9cdc091e50297"
|
||||
|
|
Loading…
Reference in a new issue