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 = { ...@@ -41,6 +41,11 @@ module.exports = {
'node_modules', 'node_modules',
'\\.(css|scss|json)$', '\\.(css|scss|json)$',
], ],
'import/resolver': {
node: {
paths: ['app/javascript'],
},
},
}, },
rules: { rules: {
......
FROM node:8.15-alpine as node 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" \ LABEL maintainer="https://github.com/tootsuite/mastodon" \
description="Your self-hosted, globally interconnected microblogging community" 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 ...@@ -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 /usr/local/bin/npm /usr/local/bin/npm
COPY --from=node /opt/yarn-* /opt/yarn COPY --from=node /opt/yarn-* /opt/yarn
RUN apk -U upgrade \ RUN apk add --no-cache -t build-dependencies \
&& apk add -t build-dependencies \
build-base \ build-base \
icu-dev \ icu-dev \
libidn-dev \ libidn-dev \
libressl \ openssl \
libtool \ libtool \
libxml2-dev \ libxml2-dev \
libxslt-dev \ libxslt-dev \
postgresql-dev \ postgresql-dev \
protobuf-dev \ protobuf-dev \
python \ python \
&& apk add \ && apk add --no-cache \
ca-certificates \ ca-certificates \
ffmpeg \ ffmpeg \
file \ file \
...@@ -64,7 +63,7 @@ RUN apk -U upgrade \ ...@@ -64,7 +63,7 @@ RUN apk -U upgrade \
&& make install \ && make install \
&& libtool --finish /usr/local/lib \ && libtool --finish /usr/local/lib \
&& cd /mastodon \ && cd /mastodon \
&& rm -rf /tmp/* /var/cache/apk/* && rm -rf /tmp/*
COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/ COPY Gemfile Gemfile.lock package.json yarn.lock .yarnclean /mastodon/
......
...@@ -107,10 +107,10 @@ group :production, :test do ...@@ -107,10 +107,10 @@ group :production, :test do
end end
group :test do group :test do
gem 'capybara', '~> 3.12' gem 'capybara', '~> 3.13'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.9' gem 'faker', '~> 1.9'
gem 'microformats', '~> 4.0' gem 'microformats', '~> 4.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.16', require: false gem 'simplecov', '~> 0.16', require: false
......
...@@ -126,7 +126,7 @@ GEM ...@@ -126,7 +126,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.12.0) capybara (3.13.2)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
...@@ -266,10 +266,10 @@ GEM ...@@ -266,10 +266,10 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.1.1) http-form_data (2.1.1)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httplog (1.2.0) httplog (1.2.1)
rack (>= 1.0) rack (>= 1.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.5.2) i18n (1.5.3)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.28) i18n-tasks (0.9.28)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
...@@ -335,9 +335,9 @@ GEM ...@@ -335,9 +335,9 @@ GEM
redis (>= 3.0.5) redis (>= 3.0.5)
memory_profiler (0.9.12) memory_profiler (0.9.12)
method_source (0.9.2) method_source (0.9.2)
microformats (4.0.7) microformats (4.1.0)
json json (~> 2.1)
nokogiri nokogiri (~> 1.8, >= 1.8.3)
mime-types (3.2.2) mime-types (3.2.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812) mime-types-data (3.2018.0812)
...@@ -455,7 +455,7 @@ GEM ...@@ -455,7 +455,7 @@ GEM
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4) rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.2) rails-i18n (5.1.3)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
...@@ -669,7 +669,7 @@ DEPENDENCIES ...@@ -669,7 +669,7 @@ DEPENDENCIES
capistrano-rails (~> 1.4) capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 3.12) capybara (~> 3.13)
charlock_holmes (~> 0.7.6) charlock_holmes (~> 0.7.6)
chewy (~> 5.0) chewy (~> 5.0)
cld3 (~> 3.2.3) cld3 (~> 3.2.3)
...@@ -709,7 +709,7 @@ DEPENDENCIES ...@@ -709,7 +709,7 @@ DEPENDENCIES
makara (~> 0.4) makara (~> 0.4)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
microformats (~> 4.0) microformats (~> 4.1)
mime-types (~> 3.2) mime-types (~> 3.2)
net-ldap (~> 0.10) net-ldap (~> 0.10)
nokogiri (~> 1.10) nokogiri (~> 1.10)
......
...@@ -127,7 +127,7 @@ You can open issues for bugs you've found or features you think are missing. You ...@@ -127,7 +127,7 @@ You can open issues for bugs you've found or features you think are missing. You
## License ## 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. 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 ...@@ -52,11 +52,12 @@ class AccountsController < ApplicationController
private private
def show_pinned_statuses? 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 end
def filtered_statuses def filtered_statuses
default_statuses.tap do |statuses| default_statuses.tap do |statuses|
statuses.merge!(hashtag_scope) if tag_requested?
statuses.merge!(only_media_scope) if media_requested? statuses.merge!(only_media_scope) if media_requested?
statuses.merge!(no_replies_scope) unless replies_requested? statuses.merge!(no_replies_scope) unless replies_requested?
end end
...@@ -78,12 +79,15 @@ class AccountsController < ApplicationController ...@@ -78,12 +79,15 @@ class AccountsController < ApplicationController
Status.without_replies Status.without_replies
end end
def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tag].downcase)&.id)
end
def set_account def set_account
@account = Account.find_local!(params[:username]) @account = Account.find_local!(params[:username])
end end
def older_url 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) pagination_url(max_id: @statuses.last.id)
end end
...@@ -92,7 +96,9 @@ class AccountsController < ApplicationController ...@@ -92,7 +96,9 @@ class AccountsController < ApplicationController
end end
def pagination_url(max_id: nil, min_id: nil) 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) short_account_media_url(@account, max_id: max_id, min_id: min_id)
elsif replies_requested? elsif replies_requested?
short_account_with_replies_url(@account, max_id: max_id, min_id: min_id) short_account_with_replies_url(@account, max_id: max_id, min_id: min_id)
...@@ -109,6 +115,10 @@ class AccountsController < ApplicationController ...@@ -109,6 +115,10 @@ class AccountsController < ApplicationController
request.path.ends_with?('/with_replies') request.path.ends_with?('/with_replies')
end end
def tag_requested?
request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
end
def filtered_status_page(params) def filtered_status_page(params)
if params[:min_id].present? if params[:min_id].present?
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
......
...@@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController ...@@ -33,6 +33,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
statuses.merge!(only_media_scope) if truthy_param?(:only_media) statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies) statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs) statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?
statuses statuses
end end
...@@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController ...@@ -67,6 +68,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
Status.without_reblogs Status.without_reblogs
end end
def hashtag_scope
Status.tagged_with(Tag.find_by(name: params[:tagged])&.id)
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params) params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
end end
......
...@@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio ...@@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :store_current_location before_action :store_current_location
before_action :authenticate_resource_owner! before_action :authenticate_resource_owner!
before_action :set_body_classes
include Localized include Localized
...@@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio ...@@ -15,6 +16,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
private private
def set_body_classes
@body_classes = 'admin'
end
def store_current_location def store_current_location
store_location_for(:user, request.url) store_location_for(:user, request.url)
end 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 ...@@ -49,6 +49,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_theme, :setting_theme,
:setting_hide_network, :setting_hide_network,
:setting_aggregate_reblogs, :setting_aggregate_reblogs,
:setting_show_application,
notification_emails: %i(follow follow_request reblog favourite mention digest report), notification_emails: %i(follow follow_request reblog favourite mention digest report),
interactions: %i(must_be_follower must_be_following) interactions: %i(must_be_follower must_be_following)
) )
......
...@@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController ...@@ -32,6 +32,6 @@ class Settings::ProfilesController < Settings::BaseController
end end
def set_account def set_account
@account = current_user.account @account = current_account
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class Settings::SessionsController < Settings::BaseController class Settings::SessionsController < Settings::BaseController
before_action :authenticate_user!
before_action :set_session, only: :destroy before_action :set_session, only: :destroy
def destroy def destroy
......
...@@ -170,7 +170,7 @@ module StreamEntriesHelper ...@@ -170,7 +170,7 @@ module StreamEntriesHelper
when 'public' when 'public'
fa_icon 'globe fw' fa_icon 'globe fw'
when 'unlisted' when 'unlisted'
fa_icon 'unlock-alt fw' fa_icon 'unlock fw'
when 'private' when 'private'
fa_icon 'lock fw' fa_icon 'lock fw'
when 'direct' when 'direct'
......
...@@ -22,7 +22,7 @@ export function clearAlert() { ...@@ -22,7 +22,7 @@ export function clearAlert() {
}; };
}; };
export function showAlert(title, message) { export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
return { return {
type: ALERT_SHOW, type: ALERT_SHOW,
title, title,
...@@ -44,6 +44,6 @@ export function showAlertForError(error) { ...@@ -44,6 +44,6 @@ export function showAlertForError(error) {
return showAlert(title, message); return showAlert(title, message);
} else { } else {
console.error(error); console.error(error);
return showAlert(messages.unexpectedTitle, messages.unexpectedMessage); return showAlert();
} }
} }
...@@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image'; ...@@ -8,6 +8,8 @@ import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer'; import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines'; import { updateTimeline } from './timelines';
import { showAlertForError } from './alerts'; import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';
let cancelFetchComposeSuggestionsAccounts; let cancelFetchComposeSuggestionsAccounts;
...@@ -49,6 +51,10 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' ...@@ -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_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; 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) { export function changeCompose(text) {
return { return {
type: COMPOSE_CHANGE, type: COMPOSE_CHANGE,
...@@ -184,20 +190,32 @@ export function submitComposeFail(error) { ...@@ -184,20 +190,32 @@ export function submitComposeFail(error) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { 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; return;
} }
dispatch(uploadComposeRequest()); dispatch(uploadComposeRequest());
resizeImage(files[0]).then(file => { for (const [i, f] of Array.from(files).entries()) {
const data = new FormData(); if (media.size + i > 3) break;
data.append('file', file);
resizeImage(f).then(file => {
return api(getState).post('/api/v1/media', data, { const data = new FormData();
onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)), data.append('file', file);
}).then(({ data }) => dispatch(uploadComposeSuccess(data)));
}).catch(error => dispatch(uploadComposeFail(error))); 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 { ...@@ -88,7 +88,7 @@ class Account extends ImmutablePureComponent {
if (requested) { if (requested) {
buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />; buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
} else if (blocking) { } 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) { } else if (muting) {
let hidingNotificationsButton; let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) { if (account.getIn(['relationship', 'muting_notifications'])) {
......
...@@ -2,6 +2,7 @@ import React from 'react'; ...@@ -2,6 +2,7 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Icon from 'mastodon/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
...@@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent { ...@@ -24,7 +25,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<li key={attachment.get('id')}> <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> </li>
); );
})} })}
...@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent { ...@@ -36,7 +37,7 @@ export default class AttachmentList extends ImmutablePureComponent {
return ( return (
<div className='attachment-list'> <div className='attachment-list'>
<div className='attachment-list__icon'> <div className='attachment-list__icon'>
<i className='fa fa-link' /> <Icon id='link' />
</div> </div>
<ul className='attachment-list__list'> <ul className='attachment-list__list'>
......
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
export default class ColumnBackButton extends React.PureComponent { export default class ColumnBackButton extends React.PureComponent {
...@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent { ...@@ -19,7 +20,7 @@ export default class ColumnBackButton extends React.PureComponent {
render () { render () {
return ( return (
<button onClick={this.handleClick} className='column-back-button'> <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' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button> </button>
); );
......
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ColumnBackButton from './column_back_button'; import ColumnBackButton from './column_back_button';
import Icon from 'mastodon/components/icon';
export default class ColumnBackButtonSlim extends ColumnBackButton { export default class ColumnBackButtonSlim extends ColumnBackButton {
...@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton { ...@@ -8,7 +9,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
return ( return (
<div className='column-back-button--slim'> <div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'> <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' /> <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div> </div>
</div> </div>
......
...@@ -2,6 +2,7 @@ import React from 'react'; ...@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import Icon from 'mastodon/components/icon';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
...@@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent { ...@@ -109,22 +110,22 @@ class ColumnHeader extends React.PureComponent {
} }
if (multiColumn && pinned) { 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>;