Commit d87f63a3 authored by Lukas Fülling's avatar Lukas Fülling 🗓

Merge branch 'master' of github.com:tootsuite/mastodon

 Conflicts:
	app/javascript/mastodon/features/getting_started/index.js
	app/models/user.rb
	config/locales/devise.ja.yml
	config/locales/en.yml
	config/locales/simple_form.en.yml
	config/settings.yml
parents 2815c9d5 d09ce6d8
......@@ -41,6 +41,11 @@ module.exports = {
'node_modules',
'\\.(css|scss|json)$',
],
'import/resolver': {
node: {
paths: ['app/javascript'],
},
},
},
rules: {
......
FROM node:8.15-alpine as node
FROM ruby:2.6-alpine3.8
FROM ruby:2.6-alpine3.9
LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="Your self-hosted, globally interconnected microblogging community"
......@@ -24,19 +24,18 @@ COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/bin/npm /usr/local/bin/npm
COPY --from=node /opt/yarn-* /opt/yarn
RUN apk -U upgrade \
&& apk add -t build-dependencies \
RUN apk add --no-cache -t build-dependencies \
build-base \
icu-dev \
libidn-dev \
libressl \
openssl \
libtool \
libxml2-dev \
libxslt-dev \
postgresql-dev \
protobuf-dev \
python \
&& apk add \
&& apk add --no-cache \
ca-certificates \
ffmpeg \
file \
......@@ -64,7 +63,7 @@ RUN apk -U upgrade \
&& make install \
&& libtool --finish /usr/local/lib \
&& cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/*
&& rm -rf /tmp/*
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
......
......@@ -107,10 +107,10 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 3.12'
gem 'capybara', '~> 3.13'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.0'
gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false
......
......@@ -126,7 +126,7 @@ GEM
sshkit (~> 1.3)
capistrano-yarn (2.0.2)
capistrano (~> 3.0)
capybara (3.12.0)
capybara (3.13.2)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
......@@ -266,10 +266,10 @@ GEM
domain_name (~> 0.5)
http-form_data (2.1.1)
http_accept_language (2.1.1)
httplog (1.2.0)
httplog (1.2.1)
rack (>= 1.0)
rainbow (>= 2.0.0)
i18n (1.5.2)
i18n (1.5.3)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.28)
activesupport (>= 4.0.2)
......@@ -335,9 +335,9 @@ GEM
redis (>= 3.0.5)
memory_profiler (0.9.12)
method_source (0.9.2)
microformats (4.0.7)
json
nokogiri
microformats (4.1.0)
json (~> 2.1)
nokogiri (~> 1.8, >= 1.8.3)
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
......@@ -455,7 +455,7 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.2)
rails-i18n (5.1.3)
i18n (>= 0.7, < 2)
railties (>= 5.0, < 6)
rails-settings-cached (0.6.6)
......@@ -669,7 +669,7 @@ DEPENDENCIES
capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 3.12)
capybara (~> 3.13)
charlock_holmes (~> 0.7.6)
chewy (~> 5.0)
cld3 (~> 3.2.3)
......@@ -709,7 +709,7 @@ DEPENDENCIES
makara (~> 0.4)
mario-redis-lock (~> 1.2)
memory_profiler
microformats (~> 4.0)
microformats (~> 4.1)
mime-types (~> 3.2)
net-ldap (~> 0.10)
nokogiri (~> 1.10)
......
......@@ -127,7 +127,7 @@ You can open issues for bugs you've found or features you think are missing. You
## License
Copyright (C) 2016-2018 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
Copyright (C) 2016-2019 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
......
......@@ -52,11 +52,12 @@ class AccountsController < ApplicationController
private
def show_pinned_statuses?
[replies_requested?, media_requested?, params[:max_id].present?, params[:min_id].present?].none?
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
end
def filtered_statuses
default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested?
end
......@@ -78,12 +79,15 @@ class AccountsController < ApplicationController
Status.without_replies
end
def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
end
def set_account
@account = Account.find_local!(params[:username])
end
def older_url
::Rails.logger.info("older: max_id #{@statuses.last.id}, url #{pagination_url(max_id: @statuses.last.id)}")
pagination_url(max_id: @statuses.last.id)
end
......@@ -92,7 +96,9 @@ class AccountsController < ApplicationController
end
def pagination_url(max_id: nil, min_id: nil)
if media_requested?
if tag_requested?
short_account_tag_url(@account, params[:tag], max_id: max_id, min_id: min_id)
elsif media_requested?
short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
......@@ -109,6 +115,10 @@ class AccountsController < ApplicationController
request.path.ends_with?('/with_replies')
end
def tag_requested?
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
def filtered_status_page(params)
if params[:min_id].present?
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
......
......@@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses
end
......@@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_reblogs
end
def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
end
def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end
......
......@@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :store_current_location
before_action :authenticate_resource_owner!
before_action :set_body_classes
include Localized
......@@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
private
def set_body_classes
@body_classes = 'admin'
end
def store_current_location
store_location_for(:user, request.url)
end
......
# frozen_string_literal: true
class Settings::FeaturedTagsController < Settings::BaseController
layout 'admin'
before_action :authenticate_user!
before_action :set_featured_tags, only: :index
before_action :set_featured_tag, except: [:index, :create]
before_action :set_most_used_tags, only: :index
def index
@featured_tag = FeaturedTag.new
end
def create
@featured_tag = current_account.featured_tags.new(featured_tag_params)
@featured_tag.reset_data
if @featured_tag.save
redirect_to settings_featured_tags_path
else
set_featured_tags
set_most_used_tags
render :index
end
end
def destroy
@featured_tag.destroy!
redirect_to settings_featured_tags_path
end
private
def set_featured_tag
@featured_tag = current_account.featured_tags.find(params[:id])
end
def set_featured_tags
@featured_tags = current_account.featured_tags.order(statuses_count: :desc).reject(&:new_record?)
end
def set_most_used_tags
@most_used_tags = Tag.most_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
end
def featured_tag_params
params.require(:featured_tag).permit(:name)
end
end
......@@ -49,6 +49,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_theme,
:setting_hide_network,
:setting_aggregate_reblogs,
:setting_show_application,
notification_emails: %i(follow follow_request reblog favourite mention digest report),
interactions: %i(must_be_follower must_be_following)
)
......
......@@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController
end
def set_account
@account = current_user.account
@account = current_account
end
end
# frozen_string_literal: true
class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :destroy
def destroy
......
......@@ -170,7 +170,7 @@ module StreamEntriesHelper
when 'public'
fa_icon 'globe fw'
when 'unlisted'
fa_icon 'unlock-alt fw'
fa_icon 'unlock fw'
when 'private'
fa_icon 'lock fw'
when 'direct'
......
......@@ -22,7 +22,7 @@ export function clearAlert() {
};
};
export function showAlert(title, message) {
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
return {
type: ALERT_SHOW,
title,
......@@ -44,6 +44,6 @@ export function showAlertForError(error) {
return showAlert(title, message);
} else {
console.error(error);
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage);
return showAlert();
}
}
......@@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';
let cancelFetchComposeSuggestionsAccounts;
......@@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
});
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
......@@ -184,20 +190,32 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function (dispatch, getState) {
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']);
const total = Array.from(files).reduce((a, v) => a + v.size, 0);
const progress = new Array(files.length).fill(0);
if (files.length + media.size > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit));
return;
}
dispatch(uploadComposeRequest());
resizeImage(files[0]).then(file => {
const data = new FormData();
data.append('file', file);
return api(getState).post('/api/v1/media', data, {
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}).catch(error => dispatch(uploadComposeFail(error)));
for (const [i, f] of Array.from(files).entries()) {
if (media.size + i > 3) break;
resizeImage(f).then(file => {
const data = new FormData();
data.append('file', file);
return api(getState).post('/api/v1/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}).catch(error => dispatch(uploadComposeFail(error)));
};
};
};
......
......@@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent {
if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
......
......@@ -2,6 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
......@@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<li key={attachment.get('id')}>
<a href={displayUrl} target='_blank' rel='noopener'><i className='fa fa-link' /> {filename(displayUrl)}</a>
<a href={displayUrl} target='_blank' rel='noopener'><Icon id='link' /> {filename(displayUrl)}</a>
</li>
);
})}
......@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return (
<div className='attachment-list'>
<div className='attachment-list__icon'>
<i className='fa fa-link' />
<Icon id='link' />
</div>
<ul className='attachment-list__list'>
......
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
export default class ColumnBackButton extends React.PureComponent {
......@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
render () {
return (
<button onClick={this.handleClick} className='column-back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
......
import React from 'react';
import { FormattedMessage } from 'react-intl';
import ColumnBackButton from './column_back_button';
import Icon from 'mastodon/components/icon';
export default class ColumnBackButtonSlim extends ColumnBackButton {
......@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
return (
<div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>
</div>
......
......@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
......@@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent {
}
if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><i className='fa fa-chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><i className='fa fa-chevron-right' /></button>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='text-btn column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='text-btn column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' /></button>
</div>
);
} else if (multiColumn) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><i className='fa fa fa-plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
}
if (!pinned && (multiColumn || showBackButton)) {
backButton = (
<button onClick={this.handleBackClick} className='column-header__back-button'>
<i className='fa fa-fw fa-chevron-left column-back-button__icon' />
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>
);
......@@ -140,7 +141,7 @@ class ColumnHeader extends React.PureComponent {
}
if (children || multiColumn) {
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><i className='fa fa-sliders' /></button>;
collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='sliders' /></button>;
}
const hasTitle = icon && title;
......@@ -150,7 +151,7 @@ class ColumnHeader extends React.PureComponent {
<h1 className={buttonClassName}>
{hasTitle && (
<button onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
<Icon id={icon} fixedWidth className='column-header__icon' />
{title}
</button>
)}
......
......@@ -32,7 +32,7 @@ class Account extends ImmutablePureComponent {
</span>
<div className='domain__buttons'>
<IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
<IconButton active icon='unlock' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} />
</div>
</div>
</div>
......
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default class Icon extends React.PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
className: PropTypes.string,
fixedWidth: PropTypes.bool,
};
render () {
const { id, className, fixedWidth, ...other } = this.props;
return (
<i role='img' className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })} {...other} />
);
}
}
......@@ -3,6 +3,7 @@ import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
export default class IconButton extends React.PureComponent {
......@@ -86,7 +87,7 @@ export default class IconButton extends React.PureComponent {
style={style}
tabIndex={tabIndex}
>
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} fixedWidth aria-hidden='true' />
</button>
);
}
......@@ -104,7 +105,7 @@ export default class IconButton extends React.PureComponent {
style={style}
tabIndex={tabIndex}
>
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
<Icon id={icon} style={{ transform: `rotate(${rotate}deg)` }} fixedWidth aria-hidden='true' />
</button>
)}
</Motion>
......
......@@ -65,7 +65,7 @@ export default class IntersectionObserverArticle extends React.Component {
}
updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting && !this.entry.isIntersecting) {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
......
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
......@@ -25,7 +26,7 @@ class LoadGap extends React.PureComponent {
return (
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<i className='fa fa-ellipsis-h' />
<Icon id='ellipsis-h' />
</button>
);
}
......
......@@ -15,6 +15,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
......@@ -204,7 +205,7 @@ class Status extends ImmutablePureComponent {
if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-thumb-tack status__prepend-icon' /></div>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
</div>
);
......@@ -213,7 +214,7 @@ class Status extends ImmutablePureComponent {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
......
......@@ -5,6 +5,7 @@ import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
......@@ -160,7 +161,7 @@ export default class StatusContent extends React.PureComponent {
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' />
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><Icon id='angle-right' fixedWidth />
</button>
);
......
......@@ -7,6 +7,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from