Merge tag 'v2.6.1' into HEAD

This commit is contained in:
Mike Barnes 2018-11-05 11:40:51 +11:00
commit 654fcce70e
636 changed files with 13263 additions and 6584 deletions

View file

@ -1,52 +0,0 @@
import api from '../api';
export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST';
export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS';
export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL';
export function fetchStatusCard(id) {
return (dispatch, getState) => {
if (getState().getIn(['cards', id], null) !== null) {
return;
}
dispatch(fetchStatusCardRequest(id));
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
if (!response.data.url) {
return;
}
dispatch(fetchStatusCardSuccess(id, response.data));
}).catch(error => {
dispatch(fetchStatusCardFail(id, error));
});
};
};
export function fetchStatusCardRequest(id) {
return {
type: STATUS_CARD_FETCH_REQUEST,
id,
skipLoading: true,
};
};
export function fetchStatusCardSuccess(id, card) {
return {
type: STATUS_CARD_FETCH_SUCCESS,
id,
card,
skipLoading: true,
};
};
export function fetchStatusCardFail(id, error) {
return {
type: STATUS_CARD_FETCH_FAIL,
id,
error,
skipLoading: true,
skipAlert: true,
};
};

View file

@ -56,7 +56,7 @@ export function changeCompose(text) {
};
};
export function replyCompose(status, router) {
export function replyCompose(status, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_REPLY,
@ -64,7 +64,7 @@ export function replyCompose(status, router) {
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
routerHistory.push('/statuses/new');
}
};
};
@ -81,7 +81,7 @@ export function resetCompose() {
};
};
export function mentionCompose(account, router) {
export function mentionCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_MENTION,
@ -89,12 +89,12 @@ export function mentionCompose(account, router) {
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
routerHistory.push('/statuses/new');
}
};
};
export function directCompose(account, router) {
export function directCompose(account, routerHistory) {
return (dispatch, getState) => {
dispatch({
type: COMPOSE_DIRECT,
@ -102,12 +102,12 @@ export function directCompose(account, router) {
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
routerHistory.push('/statuses/new');
}
};
};
export function submitCompose() {
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
const status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']);
@ -133,21 +133,24 @@ export function submitCompose() {
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns
// To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = (timelineId) => {
const insertIfOnline = timelineId => {
if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
insertIfOnline('home');
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/timelines/direct');
} else if (response.data.visibility !== 'direct') {
insertIfOnline('home');
}
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
} else if (response.data.visibility === 'direct') {
insertIfOnline('direct');
}
}).catch(function (error) {
dispatch(submitComposeFail(error));

View file

@ -0,0 +1,81 @@
import api, { getLinks } from '../api';
import {
importFetchedAccounts,
importFetchedStatuses,
importFetchedStatus,
} from './importer';
export const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT';
export const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT';
export const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST';
export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
export const mountConversations = () => ({
type: CONVERSATIONS_MOUNT,
});
export const unmountConversations = () => ({
type: CONVERSATIONS_UNMOUNT,
});
export const markConversationRead = conversationId => (dispatch, getState) => {
dispatch({
type: CONVERSATIONS_READ,
id: conversationId,
});
api(getState).post(`/api/v1/conversations/${conversationId}/read`);
};
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
dispatch(expandConversationsRequest());
const params = { max_id: maxId };
if (!maxId) {
params.since_id = getState().getIn(['conversations', 0, 'last_status']);
}
api(getState).get('/api/v1/conversations', { params })
.then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.reduce((aggr, item) => aggr.concat(item.accounts), [])));
dispatch(importFetchedStatuses(response.data.map(item => item.last_status).filter(x => !!x)));
dispatch(expandConversationsSuccess(response.data, next ? next.uri : null));
})
.catch(err => dispatch(expandConversationsFail(err)));
};
export const expandConversationsRequest = () => ({
type: CONVERSATIONS_FETCH_REQUEST,
});
export const expandConversationsSuccess = (conversations, next) => ({
type: CONVERSATIONS_FETCH_SUCCESS,
conversations,
next,
});
export const expandConversationsFail = error => ({
type: CONVERSATIONS_FETCH_FAIL,
error,
});
export const updateConversations = conversation => dispatch => {
dispatch(importFetchedAccounts(conversation.accounts));
if (conversation.last_status) {
dispatch(importFetchedStatus(conversation.last_status));
}
dispatch({
type: CONVERSATIONS_UPDATE,
conversation,
});
};

View file

@ -1,8 +1,8 @@
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
export function openDropdownMenu(id, placement) {
return { type: DROPDOWN_MENU_OPEN, id, placement };
export function openDropdownMenu(id, placement, keyboard) {
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
}
export function closeDropdownMenu(id) {

View file

@ -1,6 +1,7 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from '../../features/emoji/emoji';
import { unescapeHTML } from '../../utils/html';
import { expandSpoilers } from '../../initial_state';
const domParser = new DOMParser();
@ -13,7 +14,7 @@ export function normalizeAccount(account) {
account = { ...account };
const emojiMap = makeEmojiMap(account);
const displayName = account.display_name.length === 0 ? account.username : account.display_name;
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
account.note_emojified = emojify(account.note, emojiMap);
@ -57,7 +58,7 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = spoilerText.length > 0 || normalStatus.sensitive;
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
}
return normalStatus;

View file

@ -3,7 +3,6 @@ import openDB from '../storage/db';
import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
import { fetchStatusCard } from './cards';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
@ -86,7 +85,6 @@ export function fetchStatus(id) {
const skipLoading = getState().getIn(['statuses', id], null) !== null;
dispatch(fetchContext(id));
dispatch(fetchStatusCard(id));
if (skipLoading) {
return;

View file

@ -6,6 +6,7 @@ import {
disconnectTimeline,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters';
import { getLocale } from '../locales';
@ -31,6 +32,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));
break;
case 'filters_changed':
dispatch(fetchFilters());
break;

View file

@ -0,0 +1,52 @@
import api from '../api';
import { importFetchedAccounts } from './importer';
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
export function fetchSuggestions() {
return (dispatch, getState) => {
dispatch(fetchSuggestionsRequest());
api(getState).get('/api/v1/suggestions').then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchSuggestionsSuccess(response.data));
}).catch(error => dispatch(fetchSuggestionsFail(error)));
};
};
export function fetchSuggestionsRequest() {
return {
type: SUGGESTIONS_FETCH_REQUEST,
skipLoading: true,
};
};
export function fetchSuggestionsSuccess(accounts) {
return {
type: SUGGESTIONS_FETCH_SUCCESS,
accounts,
skipLoading: true,
};
};
export function fetchSuggestionsFail(error) {
return {
type: SUGGESTIONS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
};
export const dismissSuggestion = accountId => (dispatch, getState) => {
dispatch({
type: SUGGESTIONS_DISMISS,
id: accountId,
});
api(getState).delete(`/api/v1/suggestions/${accountId}`);
};

View file

@ -76,7 +76,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });

View file

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="foobar"
className="emojione"
src="http://example.com/emoji.png"
/>
:foobar:
</div>
`;
exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="💙"
className="emojione"
src="/emoji/1f499.svg"
/>
:foobar:
</div>
`;

View file

@ -3,7 +3,6 @@
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
<button
className="button button-secondary"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -18,7 +17,6 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
exports[`<Button /> renders a button element 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -48,7 +46,6 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button
className="button button--block"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -63,7 +60,6 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
exports[`<Button /> renders the children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -82,7 +78,6 @@ exports[`<Button /> renders the children 1`] = `
exports[`<Button /> renders the given text 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -99,7 +94,6 @@ exports[`<Button /> renders the given text 1`] = `
exports[`<Button /> renders the props.text instead of children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {

View file

@ -0,0 +1,29 @@
import React from 'react';
import renderer from 'react-test-renderer';
import AutosuggestEmoji from '../autosuggest_emoji';
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {
const emoji = {
native: '💙',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders emoji with custom url', () => {
const emoji = {
custom: true,
imageUrl: 'http://example.com/emoji.png',
native: 'foobar',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View file

@ -19,8 +19,8 @@ const messages = defineMessages({
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
@injectIntl
export default class Account extends ImmutablePureComponent {
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@ -30,6 +30,9 @@ export default class Account extends ImmutablePureComponent {
onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
onActionClick: PropTypes.func,
};
handleFollow = () => {
@ -52,8 +55,12 @@ export default class Account extends ImmutablePureComponent {
this.props.onMuteNotifications(this.props.account, false);
}
handleAction = () => {
this.props.onActionClick(this.props.account);
}
render () {
const { account, intl, hidden } = this.props;
const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
if (!account) {
return <div />;
@ -70,7 +77,9 @@ export default class Account extends ImmutablePureComponent {
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
if (onActionClick && actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);

View file

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class AvatarComposite extends React.PureComponent {
static propTypes = {
accounts: ImmutablePropTypes.list.isRequired,
animate: PropTypes.bool,
size: PropTypes.number.isRequired,
};
static defaultProps = {
animate: autoPlayGif,
};
renderItem (account, size, index) {
const { animate } = this.props;
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (index === 0) {
right = '2px';
} else if (index > 0) {
left = '2px';
}
if (index === 1) {
bottom = '2px';
} else if (index > 1) {
top = '2px';
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '2px';
}
if (index === 1 || index === 3) {
left = '2px';
}
if (index < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
const style = {
left: left,
top: top,
right: right,
bottom: bottom,
width: `${width}%`,
height: `${height}%`,
backgroundSize: 'cover',
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div key={account.get('id')} style={style} />
);
}
render() {
const { accounts, size } = this.props;
return (
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
</div>
);
}
}

View file

@ -10,8 +10,8 @@ const messages = defineMessages({
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
});
@injectIntl
export default class ColumnHeader extends React.PureComponent {
export default @injectIntl
class ColumnHeader extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -5,14 +5,24 @@ export default class DisplayName extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list,
};
render () {
const displayNameHtml = { __html: this.props.account.get('display_name_html') };
const { account, others } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
let suffix;
if (others && others.size > 1) {
suffix = `+${others.size}`;
} else {
suffix = <span className='display-name__account'>@{account.get('acct')}</span>;
}
return (
<span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
</span>
);
}

View file

@ -8,8 +8,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});
@injectIntl
export default class Account extends ImmutablePureComponent {
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
domain: PropTypes.string,

View file

@ -23,6 +23,7 @@ class DropdownMenu extends React.PureComponent {
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
@ -42,13 +43,15 @@ class DropdownMenu extends React.PureComponent {
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem) this.focusedItem.focus();
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
this.setState({ mounted: true });
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
@ -62,13 +65,10 @@ class DropdownMenu extends React.PureComponent {
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(e.currentTarget);
const index = items.indexOf(document.activeElement);
let element;
switch(e.key) {
case 'Enter':
this.handleClick(e);
break;
case 'ArrowDown':
element = items[index+1];
if (element) {
@ -96,6 +96,12 @@ class DropdownMenu extends React.PureComponent {
}
}
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e);
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
@ -120,7 +126,7 @@ class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}>
{text}
</a>
</li>
@ -170,6 +176,7 @@ export default class Dropdown extends React.PureComponent {
onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
@ -180,14 +187,14 @@ export default class Dropdown extends React.PureComponent {
id: id++,
};
handleClick = ({ target }) => {
handleClick = ({ target, type }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement);
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
@ -197,6 +204,11 @@ export default class Dropdown extends React.PureComponent {
handleKeyDown = e => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.preventDefault();
break;
case 'Escape':
this.handleClose();
break;
@ -233,7 +245,7 @@ export default class Dropdown extends React.PureComponent {
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props;
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const open = this.state.id === openDropdownId;
return (
@ -249,7 +261,7 @@ export default class Dropdown extends React.PureComponent {
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} />
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</Overlay>
</div>
);

View file

@ -6,8 +6,8 @@ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
@injectIntl
export default class LoadGap extends React.PureComponent {
export default @injectIntl
class LoadGap extends React.PureComponent {
static propTypes = {
disabled: PropTypes.bool,

View file

@ -6,7 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import { autoPlayGif, displaySensitiveMedia } from '../initial_state';
import { autoPlayGif, displayMedia } from '../initial_state';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@ -179,8 +179,8 @@ class Item extends React.PureComponent {
}
@injectIntl
export default class MediaGallery extends React.PureComponent {
export default @injectIntl
class MediaGallery extends React.PureComponent {
static propTypes = {
sensitive: PropTypes.bool,
@ -197,7 +197,7 @@ export default class MediaGallery extends React.PureComponent {
};
state = {
visible: !this.props.sensitive || displaySensitiveMedia,
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
};
componentWillReceiveProps (nextProps) {

View file

@ -86,8 +86,8 @@ export const timeAgoString = (intl, date, now, year) => {
return relativeTime;
};
@injectIntl
export default class RelativeTimestamp extends React.Component {
export default @injectIntl
class RelativeTimestamp extends React.Component {
static propTypes = {
intl: PropTypes.object.isRequired,

View file

@ -3,11 +3,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import AvatarComposite from './avatar_composite';
import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from '../features/ui/util/async-components';
@ -35,8 +37,8 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
return values.join(', ');
};
@injectIntl
export default class Status extends ImmutablePureComponent {
export default @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@ -45,6 +47,8 @@ export default class Status extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@ -60,6 +64,7 @@ export default class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
};
@ -74,6 +79,11 @@ export default class Status extends ImmutablePureComponent {
]
handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (!this.context.router) {
return;
}
@ -158,7 +168,7 @@ export default class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured } = this.props;
const { intl, hidden, featured, otherAccounts, unread } = this.props;
let { status, account, ...other } = this.props;
@ -247,11 +257,21 @@ export default class Status extends ImmutablePureComponent {
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (
<Card
onOpenMedia={this.props.onOpenMedia}
card={status.get('card')}
compact
/>
);
}
if (account === undefined || account === null) {
if (otherAccounts) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
} else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />;
}else{
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
@ -269,10 +289,10 @@ export default class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@ -281,11 +301,11 @@ export default class Status extends ImmutablePureComponent {
{statusAvatar}
</div>
<DisplayName account={status.get('account')} />
<DisplayName account={status.get('account')} others={otherAccounts} />
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
{media}

View file

@ -42,8 +42,8 @@ const obfuscatedCount = count => {
}
};
@injectIntl
export default class StatusActionBar extends ImmutablePureComponent {
export default @injectIntl
class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -6,6 +6,8 @@ import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
export default class StatusContent extends React.PureComponent {
static contextTypes = {
@ -17,10 +19,12 @@ export default class StatusContent extends React.PureComponent {
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
};
state = {
hidden: true,
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
};
_updateStatusLinks () {
@ -53,6 +57,16 @@ export default class StatusContent extends React.PureComponent {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
if (
this.props.collapsable
&& this.props.onClick
&& this.state.collapsed === null
&& node.clientHeight > MAX_HEIGHT
&& this.props.status.get('spoiler_text').length === 0
) {
this.setState({ collapsed: true });
}
}
componentDidMount () {
@ -113,6 +127,11 @@ export default class StatusContent extends React.PureComponent {
}
}
handleCollapsedClick = (e) => {
e.preventDefault();
this.setState({ collapsed: !this.state.collapsed });
}
setRef = (c) => {
this.node = c;
}
@ -132,12 +151,19 @@ export default class StatusContent extends React.PureComponent {
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': this.state.collapsed === true,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
}
const readMoreButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick} key='read-more'>
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' />
</button>
);
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
@ -167,17 +193,24 @@ export default class StatusContent extends React.PureComponent {
</div>
);
} else if (this.props.onClick) {
return (
const output = [
<div
ref={this.setRef}
tabIndex='0'
key='content'
className={classNames}
style={directionStyle}
dangerouslySetInnerHTML={content}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
);
/>,
];
if (this.state.collapsed) {
output.push(readMoreButton);
}
return output;
} else {
return (
<div

View file

@ -8,15 +8,16 @@ const mapStateToProps = state => ({
isModalOpen: state.get('modal').modalType === 'ACTIONS',
dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
openDropdownId: state.getIn(['dropdown_menu', 'openId']),
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
});
const mapDispatchToProps = (dispatch, { status, items }) => ({
onOpen(id, onItemClick, dropdownPlacement) {
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
dispatch(isUserTouching() ? openModal('ACTIONS', {
status,
actions: items,
onClick: onItemClick,
}) : openDropdownMenu(id, dropdownPlacement));
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
},
onClose(id) {
dispatch(closeModal());

View file

@ -36,6 +36,8 @@ const messages = defineMessages({
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
});
const makeMapStateToProps = () => {
@ -51,7 +53,18 @@ const makeMapStateToProps = () => {
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
dispatch(replyCompose(status, router));
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
}));
} else {
dispatch(replyCompose(status, router));
}
});
},
onModalReblog (status) {

View file

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { Link } from 'react-router-dom';
import { NavLink } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state';
import { shortNumberFormat } from '../../../utils/numbers';
@ -36,8 +36,8 @@ const messages = defineMessages({
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
});
@injectIntl
export default class ActionBar extends React.PureComponent {
export default @injectIntl
class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
@ -60,6 +60,13 @@ export default class ActionBar extends React.PureComponent {
});
}
isStatusesPageActive = (match, location) => {
if (!match) {
return false;
}
return !location.pathname.match(/\/(followers|following)\/?$/);
}
render () {
const { account, intl } = this.props;
@ -147,20 +154,20 @@ export default class ActionBar extends React.PureComponent {
<div className='account__action-bar'>
<div className='account__action-bar-links'>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<FormattedMessage id='account.posts' defaultMessage='Toots' />
<strong>{shortNumberFormat(account.get('statuses_count'))}</strong>
</Link>
</NavLink>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<FormattedMessage id='account.follows' defaultMessage='Follows' />
<strong>{shortNumberFormat(account.get('following_count'))}</strong>
</Link>
</NavLink>
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<FormattedMessage id='account.followers' defaultMessage='Followers' />
<strong>{shortNumberFormat(account.get('followers_count'))}</strong>
</Link>
</NavLink>
</div>
<div className='account__action-bar-dropdown'>

View file

@ -15,8 +15,18 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
});
const dateFormatOptions = {
month: 'short',
day: 'numeric',
year: 'numeric',
hour12: false,
hour: '2-digit',
minute: '2-digit',
};
class Avatar extends ImmutablePureComponent {
static propTypes = {
@ -65,8 +75,8 @@ class Avatar extends ImmutablePureComponent {
}
@injectIntl
export default class Header extends ImmutablePureComponent {
export default @injectIntl
class Header extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
@ -163,7 +173,10 @@ export default class Header extends ImmutablePureComponent {
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>
</dl>
))}
</div>

View file

@ -2,7 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Permalink from '../../../components/permalink';
import { displaySensitiveMedia } from '../../../initial_state';
import { displayMedia } from '../../../initial_state';
export default class MediaItem extends ImmutablePureComponent {
@ -11,7 +11,7 @@ export default class MediaItem extends ImmutablePureComponent {
};
state = {
visible: !this.props.media.getIn(['status', 'sensitive']) || displaySensitiveMedia,
visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
};
handleClick = () => {

View file

@ -43,8 +43,8 @@ class LoadMoreMedia extends ImmutablePureComponent {
}
@connect(mapStateToProps)
export default class AccountGallery extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class AccountGallery extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -23,8 +23,8 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
};
};
@connect(mapStateToProps)
export default class AccountTimeline extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -20,9 +20,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'blocks', 'items']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Blocks extends ImmutablePureComponent {
class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle';
@injectIntl
export default class ColumnSettings extends React.PureComponent {
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,

View file

@ -25,9 +25,9 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => {
};
};
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class CommunityTimeline extends React.PureComponent {
class CommunityTimeline extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -17,8 +17,8 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
});
@injectIntl
export default class ActionBar extends React.PureComponent {
export default @injectIntl
class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,

View file

@ -27,8 +27,12 @@ const messages = defineMessages({
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
});
@injectIntl
export default class ComposeForm extends ImmutablePureComponent {
export default @injectIntl
class ComposeForm extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
intl: PropTypes.object.isRequired,
@ -84,7 +88,7 @@ export default class ComposeForm extends ImmutablePureComponent {
return;
}
this.props.onSubmit();
this.props.onSubmit(this.context.router ? this.context.router.history : null);
}
onSuggestionsClearRequested = () => {

View file

@ -261,6 +261,7 @@ class EmojiPickerMenu extends React.PureComponent {
skin={skinTone}
showPreview={false}
backgroundImageFn={backgroundImageFn}
autoFocus
emojiTooltip
/>
@ -277,8 +278,8 @@ class EmojiPickerMenu extends React.PureComponent {
}
@injectIntl
export default class EmojiPickerDropdown extends React.PureComponent {
export default @injectIntl
class EmojiPickerDropdown extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,

View file

@ -149,8 +149,8 @@ class PrivacyDropdownMenu extends React.PureComponent {
}
@injectIntl
export default class PrivacyDropdown extends React.PureComponent {
export default @injectIntl
class PrivacyDropdown extends React.PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
@ -164,7 +164,7 @@ export default class PrivacyDropdown extends React.PureComponent {
state = {
open: false,
placement: null,
placement: 'bottom',
};
handleToggle = ({ target }) => {

View file

@ -12,8 +12,8 @@ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
@injectIntl
export default class ReplyIndicator extends ImmutablePureComponent {
export default @injectIntl
class ReplyIndicator extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -43,8 +43,8 @@ class SearchPopout extends React.PureComponent {
}
@injectIntl
export default class Search extends React.PureComponent {
export default @injectIntl
class Search extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,

View file

@ -1,19 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from '../../../components/hashtag';
export default class SearchResults extends ImmutablePureComponent {
const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
});
export default @injectIntl
class SearchResults extends ImmutablePureComponent {
static propTypes = {
results: ImmutablePropTypes.map.isRequired,
suggestions: ImmutablePropTypes.list.isRequired,
fetchSuggestions: PropTypes.func.isRequired,
dismissSuggestion: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
componentDidMount () {
this.props.fetchSuggestions();
}
render () {
const { results } = this.props;
const { intl, results, suggestions, dismissSuggestion } = this.props;
if (results.isEmpty() && !suggestions.isEmpty()) {
return (
<div className='search-results'>
<div className='trends'>
<div className='trends__header'>
<i className='fa fa-user-plus fa-fw' />
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
</div>
{suggestions && suggestions.map(accountId => (
<AccountContainer
key={accountId}
id={accountId}
actionIcon='times'
actionTitle={intl.formatMessage(messages.dismissSuggestion)}
onActionClick={dismissSuggestion}
/>
))}
</div>
</div>
);
}
let accounts, statuses, hashtags;
let count = 0;

View file

@ -11,8 +11,12 @@ const messages = defineMessages({
description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' },
});
@injectIntl
export default class Upload extends ImmutablePureComponent {
export default @injectIntl
class Upload extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
media: ImmutablePropTypes.map.isRequired,
@ -37,14 +41,16 @@ export default class Upload extends ImmutablePureComponent {
handleSubmit = () => {
this.handleInputBlur();
this.props.onSubmit();
this.props.onSubmit(this.context.router.history);
}
handleUndoClick = () => {
handleUndoClick = e => {
e.stopPropagation();
this.props.onUndo(this.props.media.get('id'));
}
handleFocalPointClick = () => {
handleFocalPointClick = e => {
e.stopPropagation();
this.props.onOpenFocalPoint(this.props.media.get('id'));
}
@ -64,6 +70,10 @@ export default class Upload extends ImmutablePureComponent {
this.setState({ focused: true });
}
handleClick = () => {
this.setState({ focused: true });
}
handleInputBlur = () => {
const { dirtyDescription } = this.state;
@ -84,7 +94,7 @@ export default class Upload extends ImmutablePureComponent {
const y = ((focusY / -2) + .5) * 100;
return (
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='compose-form__upload' tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick} role='button'>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>

View file

@ -23,9 +23,9 @@ const iconStyle = {
lineHeight: '27px',
};
@connect(makeMapStateToProps)
export default @connect(makeMapStateToProps)
@injectIntl
export default class UploadButton extends ImmutablePureComponent {
class UploadButton extends ImmutablePureComponent {
static propTypes = {
disabled: PropTypes.bool,

View file

@ -33,8 +33,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(changeCompose(text));
},
onSubmit () {
dispatch(submitCompose());
onSubmit (router) {
dispatch(submitCompose(router));
},
onClearSuggestions () {

View file

@ -1,8 +1,15 @@
import { connect } from 'react-redux';
import SearchResults from '../components/search_results';
import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
const mapStateToProps = state => ({
results: state.getIn(['search', 'results']),
suggestions: state.getIn(['suggestions', 'items']),
});
export default connect(mapStateToProps)(SearchResults);
const mapDispatchToProps = dispatch => ({
fetchSuggestions: () => dispatch(fetchSuggestions()),
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
});
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);

View file

@ -22,8 +22,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(openModal('FOCAL_POINT', { id }));
},
onSubmit () {
dispatch(submitCompose());
onSubmit (router) {
dispatch(submitCompose(router));
},
});

View file

@ -12,7 +12,8 @@ import Motion from '../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import SearchResultsContainer from './containers/search_results_container';
import { changeComposing } from '../../actions/compose';
import elephantUIPlane from '../../../images/cigarmanwoman.svg';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@ -30,9 +31,9 @@ const mapStateToProps = (state, ownProps) => ({
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : ownProps.isSearchPage,
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Compose extends React.PureComponent {
class Compose extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@ -107,7 +108,7 @@ export default class Compose extends React.PureComponent {
<ComposeFormContainer />
{multiColumn && (
<div className='drawer__inner__mastodon'>
<img alt='' draggable='false' src={elephantUIPlane} />
<img alt='' draggable='false' src={mascot || elephantUIPlane} />
</div>
)}
</div>}

View file

@ -170,7 +170,7 @@ export const urlRegex = (function() {
')' +
'\\)',
'i');
// Valid end-of-path chracters (so /foo. does not gobble the period).
// Valid end-of-path characters (so /foo. does not gobble the period).
// 1. Allow =&# for empty URL parameters and other URL-join artifacts
regexen.validUrlPathEndingChars = regexSupplant(/[^#{spaces_group}\(\)\?!\*';:=\,\.\$%\[\]#{pd}~&\|@]|(?:#{validUrlBalancedParens})/i);
// Allow @ in a url, but only in the middle. Catch things like http://example.com/@user/

View file

@ -9,8 +9,8 @@ const messages = defineMessages({
settings: { id: 'home.settings', defaultMessage: 'Column settings' },
});
@injectIntl
export default class ColumnSettings extends React.PureComponent {
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,

View file

@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import StatusContainer from '../../../containers/status_container';
export default class Conversation extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired,
lastStatusId: PropTypes.string,
unread:PropTypes.bool.isRequired,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
markRead: PropTypes.func.isRequired,
};
handleClick = () => {
if (!this.context.router) {
return;
}
const { lastStatusId, unread, markRead } = this.props;
if (unread) {
markRead();
}
this.context.router.history.push(`/statuses/${lastStatusId}`);
}
handleHotkeyMoveUp = () => {
this.props.onMoveUp(this.props.conversationId);
}
handleHotkeyMoveDown = () => {
this.props.onMoveDown(this.props.conversationId);
}
render () {
const { accounts, lastStatusId, unread } = this.props;
if (lastStatusId === null) {
return null;
}
return (
<StatusContainer
id={lastStatusId}
unread={unread}
otherAccounts={accounts}
onMoveUp={this.handleHotkeyMoveUp}
onMoveDown={this.handleHotkeyMoveDown}
onClick={this.handleClick}
/>
);
}
}

View file

@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ConversationContainer from '../containers/conversation_container';
import ScrollableList from '../../../components/scrollable_list';
import { debounce } from 'lodash';
export default class ConversationsList extends ImmutablePureComponent {
static propTypes = {
conversations: ImmutablePropTypes.list.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onLoadMore: PropTypes.func,
shouldUpdateScroll: PropTypes.func,
};
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)
handleMoveUp = id => {
const elementIndex = this.getCurrentIndex(id) - 1;
this._selectChild(elementIndex);
}
handleMoveDown = id => {
const elementIndex = this.getCurrentIndex(id) + 1;
this._selectChild(elementIndex);
}
_selectChild (index) {
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) {
element.focus();
}
}
setRef = c => {
this.node = c;
}
handleLoadOlder = debounce(() => {
const last = this.props.conversations.last();
if (last && last.get('last_status')) {
this.props.onLoadMore(last.get('last_status'));
}
}, 300, { leading: true })
render () {
const { conversations, onLoadMore, ...other } = this.props;
return (
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} scrollKey='direct' ref={this.setRef}>
{conversations.map(item => (
<ConversationContainer
key={item.get('id')}
conversationId={item.get('id')}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
/>
))}
</ScrollableList>
);
}
}

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import Conversation from '../components/conversation';
import { markConversationRead } from '../../../actions/conversations';
const mapStateToProps = (state, { conversationId }) => {
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
return {
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
unread: conversation.get('unread'),
lastStatusId: conversation.get('last_status', null),
};
};
const mapDispatchToProps = (dispatch, { conversationId }) => ({
markRead: () => dispatch(markConversationRead(conversationId)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Conversation);

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import ConversationsList from '../components/conversations_list';
import { expandConversations } from '../../../actions/conversations';
const mapStateToProps = state => ({
conversations: state.getIn(['conversations', 'items']),
isLoading: state.getIn(['conversations', 'isLoading'], true),
hasMore: state.getIn(['conversations', 'hasMore'], false),
});
const mapDispatchToProps = dispatch => ({
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
});
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);

View file

@ -1,25 +1,21 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { expandDirectTimeline } from '../../actions/timelines';
import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connectDirectStream } from '../../actions/streaming';
import ConversationsListContainer from './containers/conversations_list_container';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
});
@connect(mapStateToProps)
export default @connect()
@injectIntl
export default class DirectTimeline extends React.PureComponent {
class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
@ -52,11 +48,14 @@ export default class DirectTimeline extends React.PureComponent {
componentDidMount () {
const { dispatch } = this.props;
dispatch(expandDirectTimeline());
dispatch(mountConversations());
dispatch(expandConversations());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount () {
this.props.dispatch(unmountConversations());
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
@ -68,11 +67,11 @@ export default class DirectTimeline extends React.PureComponent {
}
handleLoadMore = maxId => {
this.props.dispatch(expandDirectTimeline({ maxId }));
this.props.dispatch(expandConversations({ maxId }));
}
render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
const pinned = !!columnId;
return (
@ -88,7 +87,7 @@ export default class DirectTimeline extends React.PureComponent {
multiColumn={multiColumn}
/>
<StatusListContainer
<ConversationsListContainer
trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`}
timelineId='direct'

View file

@ -21,9 +21,9 @@ const mapStateToProps = state => ({
domains: state.getIn(['domain_lists', 'blocks', 'items']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Blocks extends ImmutablePureComponent {
class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -2,7 +2,7 @@
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
import data from './emoji_mart_data_light';
import { getData, getSanitizedData, intersect } from './emoji_utils';
import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
let originalPool = {};
let index = {};
@ -103,7 +103,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
}
}
allResults = values.map((value) => {
const searchValue = (value) => {
let aPool = pool,
aIndex = index,
length = 0;
@ -150,15 +150,23 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
}
return aIndex.results;
}).filter(a => a);
};
if (allResults.length > 1) {
results = intersect.apply(null, allResults);
} else if (allResults.length) {
results = allResults[0];
if (values.length > 1) {
results = searchValue(value);
} else {
results = [];
}
allResults = values.map(searchValue).filter(a => a);
if (allResults.length > 1) {
allResults = intersect.apply(null, allResults);
} else if (allResults.length) {
allResults = allResults[0];
}
results = uniq(results.concat(allResults));
}
if (results) {

View file

@ -21,9 +21,9 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Favourites extends ImmutablePureComponent {
class Favourites extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,

View file

@ -15,8 +15,8 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId]),
});
@connect(mapStateToProps)
export default class Favourites extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class Favourites extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -13,8 +13,8 @@ const messages = defineMessages({
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
});
@injectIntl
export default class AccountAuthorize extends ImmutablePureComponent {
export default @injectIntl
class AccountAuthorize extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,

View file

@ -20,9 +20,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class FollowRequests extends ImmutablePureComponent {
class FollowRequests extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -22,8 +22,8 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'followers', props.params.accountId, 'next']),
});
@connect(mapStateToProps)
export default class Followers extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class Followers extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -22,8 +22,8 @@ const mapStateToProps = (state, props) => ({
hasMore: !!state.getIn(['user_lists', 'following', props.params.accountId, 'next']),
});
@connect(mapStateToProps)
export default class Following extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -53,9 +53,9 @@ const badgeDisplay = (number, limit) => {
}
};
@connect(mapStateToProps, mapDispatchToProps)
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class GettingStarted extends ImmutablePureComponent {
class GettingStarted extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
@ -144,7 +144,7 @@ export default class GettingStarted extends ImmutablePureComponent {
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://github.com/tootsuite/documentation#documentation' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>
<li><a href='/auth/sign_out' data-method='delete'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></a></li>
</ul>

View file

@ -13,8 +13,8 @@ const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}`, 'unread']) > 0,
});
@connect(mapStateToProps)
export default class HashtagTimeline extends React.PureComponent {
export default @connect(mapStateToProps)
class HashtagTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle';
@injectIntl
export default class ColumnSettings extends React.PureComponent {
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,

View file

@ -19,9 +19,9 @@ const mapStateToProps = state => ({
isPartial: state.getIn(['timelines', 'home', 'items', 0], null) === null,
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class HomeTimeline extends React.PureComponent {
class HomeTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,

View file

@ -9,8 +9,8 @@ const messages = defineMessages({
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
});
@injectIntl
export default class KeyboardShortcuts extends ImmutablePureComponent {
export default @injectIntl
class KeyboardShortcuts extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,

View file

@ -31,9 +31,9 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({
onAdd: () => dispatch(addToListEditor(accountId)),
});
@connect(makeMapStateToProps, mapDispatchToProps)
export default @connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
export default class Account extends ImmutablePureComponent {
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,

View file

@ -19,9 +19,9 @@ const mapDispatchToProps = dispatch => ({
onChange: value => dispatch(changeListSuggestions(value)),
});
@connect(mapStateToProps, mapDispatchToProps)
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class Search extends React.PureComponent {
class Search extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,

View file

@ -22,9 +22,9 @@ const mapDispatchToProps = dispatch => ({
onReset: () => dispatch(resetListEditor()),
});
@connect(mapStateToProps, mapDispatchToProps)
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class ListEditor extends ImmutablePureComponent {
class ListEditor extends ImmutablePureComponent {
static propTypes = {
listId: PropTypes.string.isRequired,

View file

@ -25,9 +25,9 @@ const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class ListTimeline extends React.PureComponent {
class ListTimeline extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -20,9 +20,9 @@ const mapDispatchToProps = dispatch => ({
onSubmit: () => dispatch(submitListEditor(true)),
});
@connect(mapStateToProps, mapDispatchToProps)
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class NewListForm extends React.PureComponent {
class NewListForm extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,

View file

@ -31,9 +31,9 @@ const mapStateToProps = state => ({
lists: getOrderedLists(state),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Lists extends ImmutablePureComponent {
class Lists extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -20,9 +20,9 @@ const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'mutes', 'items']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Mutes extends ImmutablePureComponent {
class Mutes extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -27,7 +27,6 @@ export default class ColumnSettings extends React.PureComponent {
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
const pushMeta = showPushSettings && <FormattedMessage id='notifications.column_settings.push_meta' defaultMessage='This device' />;
return (
<div>
@ -40,7 +39,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div>
@ -51,7 +50,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'favourite']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div>
@ -62,7 +61,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'mention']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div>
@ -73,7 +72,7 @@ export default class ColumnSettings extends React.PureComponent {
<div className='column-settings__row'>
<SettingToggle prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} meta={pushMeta} onChange={this.onPushChange} label={pushStr} />}
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reblog']} onChange={this.onPushChange} label={pushStr} />}
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div>

View file

@ -16,8 +16,8 @@ const notificationForScreenReader = (intl, message, timestamp) => {
return output.join(', ');
};
@injectIntl
export default class Notification extends ImmutablePureComponent {
export default @injectIntl
class Notification extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@ -85,8 +85,9 @@ export default class Notification extends ImmutablePureComponent {
<div className='notification__favourite-icon-wrapper'>
<i className='fa fa-fw fa-user-plus' />
</div>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
<span title={notification.get('created_at')}>
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
</span>
</div>
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />

View file

@ -10,7 +10,6 @@ export default class SettingToggle extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired,
settingPath: PropTypes.array.isRequired,
label: PropTypes.node.isRequired,
meta: PropTypes.node,
onChange: PropTypes.func.isRequired,
}
@ -19,14 +18,13 @@ export default class SettingToggle extends React.PureComponent {
}
render () {
const { prefix, settings, settingPath, label, meta } = this.props;
const { prefix, settings, settingPath, label } = this.props;
const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-');
return (
<div className='setting-toggle'>
<Toggle id={id} checked={settings.getIn(settingPath)} onChange={this.onChange} onKeyDown={this.onKeyDown} />
<label htmlFor={id} className='setting-toggle__label'>{label}</label>
{meta && <span className='setting-meta__label'>{meta}</span>}
</div>
);
}

View file

@ -31,9 +31,9 @@ const mapStateToProps = state => ({
hasMore: state.getIn(['notifications', 'hasMore']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class Notifications extends React.PureComponent {
class Notifications extends React.PureComponent {
static propTypes = {
columnId: PropTypes.string,

View file

@ -18,9 +18,9 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class PinnedStatuses extends ImmutablePureComponent {
class PinnedStatuses extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,

View file

@ -25,9 +25,9 @@ const mapStateToProps = (state, { onlyMedia, columnId }) => {
};
};
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class PublicTimeline extends React.PureComponent {
class PublicTimeline extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -15,8 +15,8 @@ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'reblogged_by', props.params.statusId]),
});
@connect(mapStateToProps)
export default class Reblogs extends ImmutablePureComponent {
export default @connect(mapStateToProps)
class Reblogs extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,

View file

@ -12,9 +12,9 @@ const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
});
@connect()
export default @connect()
@injectIntl
export default class CommunityTimeline extends React.PureComponent {
class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,

View file

@ -7,8 +7,8 @@ import Column from '../../../components/column';
import ColumnHeader from '../../../components/column_header';
import { connectHashtagStream } from '../../../actions/streaming';
@connect()
export default class HashtagTimeline extends React.PureComponent {
export default @connect()
class HashtagTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,

View file

@ -12,9 +12,9 @@ const messages = defineMessages({
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
});
@connect()
export default @connect()
@injectIntl
export default class PublicTimeline extends React.PureComponent {
class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,

View file

@ -28,8 +28,8 @@ const messages = defineMessages({
embed: { id: 'status.embed', defaultMessage: 'Embed' },
});
@injectIntl
export default class ActionBar extends React.PureComponent {
export default @injectIntl
class ActionBar extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -59,10 +59,12 @@ export default class Card extends React.PureComponent {
card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
};
static defaultProps = {
maxDescription: 50,
compact: false,
};
state = {
@ -118,7 +120,7 @@ export default class Card extends React.PureComponent {
const content = { __html: addAutoPlay(card.get('html')) };
const { width } = this.state;
const ratio = card.get('width') / card.get('height');
const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
const height = width / ratio;
return (
<div
@ -131,25 +133,25 @@ export default class Card extends React.PureComponent {
}
render () {
const { card, maxDescription } = this.props;
const { width, embedded } = this.state;
const { card, maxDescription, compact } = this.props;
const { width, embedded } = this.state;
if (card === null) {
return null;
}
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link';
const className = classnames('status-card', { horizontal });
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height');
const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
const description = (
<div className='status-card__content'>
{title}
{!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>{provider}</span>
</div>
);
@ -174,7 +176,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__actions'>
<div>
<button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
<a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>
{horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>}
</div>
</div>
</div>
@ -184,7 +186,7 @@ export default class Card extends React.PureComponent {
return (
<div className={className} ref={this.setRef}>
{embed}
{description}
{!compact && description}
</div>
);
} else if (card.get('image')) {

View file

@ -8,7 +8,7 @@ import MediaGallery from '../../../components/media_gallery';
import AttachmentList from '../../../components/attachment_list';
import { Link } from 'react-router-dom';
import { FormattedDate, FormattedNumber } from 'react-intl';
import CardContainer from '../containers/card_container';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
@ -80,7 +80,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
);
}
} else if (status.get('spoiler_text').length === 0) {
media = <CardContainer onOpenMedia={this.props.onOpenMedia} statusId={status.get('id')} />;
media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
}
if (status.get('application')) {

View file

@ -1,8 +0,0 @@
import { connect } from 'react-redux';
import Card from '../components/card';
const mapStateToProps = (state, { statusId }) => ({
card: state.getIn(['cards', statusId], null),
});
export default connect(mapStateToProps)(Card);

View file

@ -54,6 +54,8 @@ const messages = defineMessages({
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
});
const makeMapStateToProps = () => {
@ -98,15 +100,16 @@ const makeMapStateToProps = () => {
status,
ancestorsIds,
descendantsIds,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
};
};
return mapStateToProps;
};
@injectIntl
export default @injectIntl
@connect(makeMapStateToProps)
export default class Status extends ImmutablePureComponent {
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@ -119,6 +122,7 @@ export default class Status extends ImmutablePureComponent {
ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
};
state = {
@ -157,7 +161,16 @@ export default class Status extends ImmutablePureComponent {
}
handleReplyClick = (status) => {
this.props.dispatch(replyCompose(status, this.context.router.history));
let { askReplyConfirmation, dispatch, intl } = this.props;
if (askReplyConfirmation) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
}));
} else {
dispatch(replyCompose(status, this.context.router.history));
}
}
handleModalReblog = (status) => {
@ -168,7 +181,7 @@ export default class Status extends ImmutablePureComponent {
if (status.get('reblogged')) {
this.props.dispatch(unreblog(status));
} else {
if (e.shiftKey || !boostModal) {
if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status);
} else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
@ -415,11 +428,11 @@ export default class Status extends ImmutablePureComponent {
/>
<ScrollContainer scrollKey='thread' shouldUpdateScroll={shouldUpdateScroll}>
<div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className='focusable' tabIndex='0' aria-label={textForScreenReader(intl, status, false, !status.get('hidden'))}>
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false, !status.get('hidden'))}>
<DetailedStatus
status={status}
onOpenVideo={this.handleOpenVideo}

View file

@ -13,8 +13,8 @@ const messages = defineMessages({
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});
@injectIntl
export default class BoostModal extends ImmutablePureComponent {
export default @injectIntl
class BoostModal extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,

View file

@ -35,8 +35,8 @@ const messages = defineMessages({
const shouldHideFAB = path => path.match(/^\/statuses\//);
@component => injectIntl(component, { withRef: true })
export default class ColumnsArea extends ImmutablePureComponent {
export default @(component => injectIntl(component, { withRef: true }))
class ColumnsArea extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,

View file

@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from 'react-intl';
import Button from '../../../components/button';
@injectIntl
export default class ConfirmationModal extends React.PureComponent {
export default @injectIntl
class ConfirmationModal extends React.PureComponent {
static propTypes = {
message: PropTypes.node.isRequired,

View file

@ -4,8 +4,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage, injectIntl } from 'react-intl';
import api from '../../../api';
@injectIntl
export default class EmbedModal extends ImmutablePureComponent {
export default @injectIntl
class EmbedModal extends ImmutablePureComponent {
static propTypes = {
url: PropTypes.string.isRequired,

View file

@ -19,8 +19,8 @@ const mapDispatchToProps = (dispatch, { id }) => ({
});
@connect(mapStateToProps, mapDispatchToProps)
export default class FocalPointModal extends ImmutablePureComponent {
export default @connect(mapStateToProps, mapDispatchToProps)
class FocalPointModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.map.isRequired,

View file

@ -18,8 +18,8 @@ const messages = defineMessages({
export const previewState = 'previewMediaModal';
@injectIntl
export default class MediaModal extends ImmutablePureComponent {
export default @injectIntl
class MediaModal extends ImmutablePureComponent {
static propTypes = {
media: ImmutablePropTypes.list.isRequired,
@ -149,7 +149,7 @@ export default class MediaModal extends ImmutablePureComponent {
startTime={time || 0}
onCloseVideo={onClose}
detailed
description={image.get('description')}
alt={image.get('description')}
key={image.get('url')}
/>
);

View file

@ -33,9 +33,9 @@ const mapDispatchToProps = dispatch => {
};
};
@connect(mapStateToProps, mapDispatchToProps)
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class MuteModal extends React.PureComponent {
class MuteModal extends React.PureComponent {
static propTypes = {
isSubmitting: PropTypes.bool.isRequired,

View file

@ -160,7 +160,7 @@ const PageSix = ({ admin, domain }) => {
<h1><FormattedMessage id='onboarding.page_six.almost_done' defaultMessage='Almost done...' /></h1>
{adminSection}
<p><FormattedMessage id='onboarding.page_six.github' defaultMessage='Mastodon is free open-source software. You can report bugs, request features, or contribute to the code on {github}.' values={{ github: <a href='https://github.com/tootsuite/mastodon' target='_blank' rel='noopener'>GitHub</a> }} /></p>
<p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <a href='https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p>
<p><FormattedMessage id='onboarding.page_six.apps_available' defaultMessage='There are {apps} available for iOS, Android and other platforms.' values={{ apps: <a href='https://joinmastodon.org/apps' target='_blank' rel='noopener'><FormattedMessage id='onboarding.page_six.various_app' defaultMessage='mobile apps' /></a> }} /></p>
<p><em><FormattedMessage id='onboarding.page_six.appetoot' defaultMessage='Bon Appetoot!' /></em></p>
</div>
);
@ -177,9 +177,9 @@ const mapStateToProps = state => ({
domain: state.getIn(['meta', 'domain']),
});
@connect(mapStateToProps)
export default @connect(mapStateToProps)
@injectIntl
export default class OnboardingModal extends React.PureComponent {
class OnboardingModal extends React.PureComponent {
static propTypes = {
onClose: PropTypes.func.isRequired,

View file

@ -37,9 +37,9 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
@connect(makeMapStateToProps)
export default @connect(makeMapStateToProps)
@injectIntl
export default class ReportModal extends ImmutablePureComponent {
class ReportModal extends ImmutablePureComponent {
static propTypes = {
isSubmitting: PropTypes.bool,
@ -106,6 +106,7 @@ export default class ReportModal extends ImmutablePureComponent {
onChange={this.handleCommentChange}
onKeyDown={this.handleKeyDown}
disabled={isSubmitting}
autoFocus
/>
{domain && (

View file

@ -24,9 +24,9 @@ export function getLink (index) {
return links[index].props.to;
}
@injectIntl
export default @injectIntl
@withRouter
export default class TabsBar extends React.PureComponent {
class TabsBar extends React.PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,

View file

@ -24,7 +24,7 @@ export default class VideoModal extends ImmutablePureComponent {
startTime={time}
onCloseVideo={onClose}
detailed
description={media.get('description')}
alt={media.get('description')}
/>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show more