From b7370ac8baa643d93ea727699b3b11f9d3a55bea Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 13 Aug 2017 00:44:41 +0200 Subject: ActivityPub delivery (#4566) * Deliver ActivityPub Like * Deliver ActivityPub Undo-Like * Deliver ActivityPub Create/Announce activities * Deliver ActivityPub creates from mentions * Deliver ActivityPub Block/Undo-Block * Deliver ActivityPub Accept/Reject-Follow * Deliver ActivityPub Undo-Follow * Deliver ActivityPub Follow * Deliver ActivityPub Delete activities Incidentally fix #889 * Adjust BatchedRemoveStatusService for ActivityPub * Add tests for ActivityPub workers * Add tests for FollowService * Add tests for FavouriteService, UnfollowService and PostStatusService * Add tests for ReblogService, BlockService, UnblockService, ProcessMentionsService * Add tests for AuthorizeFollowService, RejectFollowService, RemoveStatusService * Add tests for BatchedRemoveStatusService * Deliver updates to a local account to ActivityPub followers * Minor adjustments --- app/controllers/api/v1/accounts/credentials_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/controllers/api') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 073808532..90a580c33 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -10,8 +10,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController end def update - current_account.update!(account_params) @account = current_account + @account.update!(account_params) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end -- cgit From e120d09c982d0108bdcdb3dd4d3800870b9e3f8c Mon Sep 17 00:00:00 2001 From: abcang Date: Tue, 15 Aug 2017 21:14:12 +0900 Subject: Fix require_user! behavior when not logged in (#4604) --- app/controllers/api/base_controller.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'app/controllers/api') diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 105a2859d..6ede63c79 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -62,10 +62,11 @@ class Api::BaseController < ApplicationController end def require_user! - current_resource_owner - set_user_activity - rescue ActiveRecord::RecordNotFound - render json: { error: 'This method requires an authenticated user' }, status: 422 + if current_user + set_user_activity + else + render json: { error: 'This method requires an authenticated user' }, status: 422 + end end def render_empty -- cgit From 2edfdab6e6d70598a19d59f8a2f47ecae8add243 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 18 Aug 2017 17:42:59 +0900 Subject: Don't send Link header when don't know prev and next links (#4633) --- app/controllers/api/base_controller.rb | 2 +- spec/controllers/api/v1/favourites_controller_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'app/controllers/api') diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 6ede63c79..7cfe8fe71 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -43,7 +43,7 @@ class Api::BaseController < ApplicationController links = [] links << [next_path, [%w(rel next)]] if next_path links << [prev_path, [%w(rel prev)]] if prev_path - response.headers['Link'] = LinkHeader.new(links) + response.headers['Link'] = LinkHeader.new(links) unless links.empty? end def limit_param(default_limit) diff --git a/spec/controllers/api/v1/favourites_controller_spec.rb b/spec/controllers/api/v1/favourites_controller_spec.rb index 3de045377..46cf70f4d 100644 --- a/spec/controllers/api/v1/favourites_controller_spec.rb +++ b/spec/controllers/api/v1/favourites_controller_spec.rb @@ -70,8 +70,7 @@ RSpec.describe Api::V1::FavouritesController, type: :controller do it 'does not add pagination headers if not necessary' do get :index - expect(response.headers['Link'].find_link(['rel', 'next'])).to eq nil - expect(response.headers['Link'].find_link(['rel', 'prev'])).to eq nil + expect(response.headers['Link']).to eq nil end end end -- cgit From 74e5078795cd5bc8a10e2c22355379ff5ca6d21c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 21 Aug 2017 00:41:08 +0200 Subject: Fix #4637 - Re-add missing doorkeeper_authorize for /api/v1/verify_credentials (#4650) --- .../api/v1/accounts/credentials_controller.rb | 1 + .../api/v1/accounts/credentials_controller_spec.rb | 94 +++++++++++++--------- 2 files changed, 59 insertions(+), 36 deletions(-) (limited to 'app/controllers/api') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 90a580c33..bea83cd2a 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::CredentialsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read }, except: [:update] before_action -> { doorkeeper_authorize! :write }, only: [:update] before_action :require_user! diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index bc89772b9..461b8b34b 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -4,57 +4,79 @@ describe Api::V1::Accounts::CredentialsController do render_views let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } - let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } - before do - allow(controller).to receive(:doorkeeper_token) { token } - end - - describe 'GET #show' do - it 'returns http success' do - get :show - expect(response).to have_http_status(:success) + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } end - end - - describe 'PATCH #update' do - describe 'with valid data' do - before do - allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) - - patch :update, params: { - display_name: "Alice Isn't Dead", - note: "Hi!\n\nToot toot!", - avatar: fixture_file_upload('files/avatar.gif', 'image/gif'), - header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'), - } - end + describe 'GET #show' do it 'returns http success' do + get :show expect(response).to have_http_status(:success) end + end + + describe 'PATCH #update' do + describe 'with valid data' do + before do + allow(ActivityPub::UpdateDistributionWorker).to receive(:perform_async) + + patch :update, params: { + display_name: "Alice Isn't Dead", + note: "Hi!\n\nToot toot!", + avatar: fixture_file_upload('files/avatar.gif', 'image/gif'), + header: fixture_file_upload('files/attachment.jpg', 'image/jpeg'), + } + end - it 'updates account info' do - user.account.reload + it 'returns http success' do + expect(response).to have_http_status(:success) + end - expect(user.account.display_name).to eq("Alice Isn't Dead") - expect(user.account.note).to eq("Hi!\n\nToot toot!") - expect(user.account.avatar).to exist - expect(user.account.header).to exist + it 'updates account info' do + user.account.reload + + expect(user.account.display_name).to eq("Alice Isn't Dead") + expect(user.account.note).to eq("Hi!\n\nToot toot!") + expect(user.account.avatar).to exist + expect(user.account.header).to exist + end + + it 'queues up an account update distribution' do + expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id) + end end - it 'queues up an account update distribution' do - expect(ActivityPub::UpdateDistributionWorker).to have_received(:perform_async).with(user.account_id) + describe 'with invalid data' do + before do + patch :update, params: { note: 'This is too long. ' * 10 } + end + + it 'returns http unprocessable entity' do + expect(response).to have_http_status(:unprocessable_entity) + end end end + end - describe 'with invalid data' do - before do - patch :update, params: { note: 'This is too long. ' * 10 } + context 'without an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { nil } + end + + describe 'GET #show' do + it 'returns http unauthorized' do + get :show + expect(response).to have_http_status(:unauthorized) end + end - it 'returns http unprocessable entity' do - expect(response).to have_http_status(:unprocessable_entity) + describe 'PATCH #update' do + it 'returns http unauthorized' do + patch :update, params: { note: 'Foo' } + expect(response).to have_http_status(:unauthorized) end end end -- cgit From 9caa90025fd9f1ef46a74f31cefd19335e291e76 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 25 Aug 2017 01:41:18 +0200 Subject: Pinned statuses (#4675) * Pinned statuses * yarn manage:translations --- app/controllers/accounts_controller.rb | 25 +++++-- .../api/v1/accounts/statuses_controller.rb | 5 ++ app/controllers/api/v1/statuses/pins_controller.rb | 28 ++++++++ app/javascript/mastodon/actions/interactions.js | 78 ++++++++++++++++++++++ app/javascript/mastodon/components/status.js | 1 + .../mastodon/components/status_action_bar.js | 11 +++ .../mastodon/containers/status_container.js | 10 +++ .../features/status/components/action_bar.js | 11 +++ app/javascript/mastodon/features/status/index.js | 11 +++ app/javascript/mastodon/locales/ar.json | 2 + app/javascript/mastodon/locales/bg.json | 2 + app/javascript/mastodon/locales/ca.json | 2 + app/javascript/mastodon/locales/de.json | 2 + .../mastodon/locales/defaultMessages.json | 16 +++++ app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/locales/eo.json | 2 + app/javascript/mastodon/locales/es.json | 2 + app/javascript/mastodon/locales/fa.json | 2 + app/javascript/mastodon/locales/fi.json | 2 + app/javascript/mastodon/locales/fr.json | 2 + app/javascript/mastodon/locales/he.json | 2 + app/javascript/mastodon/locales/hr.json | 2 + app/javascript/mastodon/locales/hu.json | 2 + app/javascript/mastodon/locales/id.json | 2 + app/javascript/mastodon/locales/io.json | 2 + app/javascript/mastodon/locales/it.json | 2 + app/javascript/mastodon/locales/ja.json | 2 + app/javascript/mastodon/locales/ko.json | 2 + app/javascript/mastodon/locales/nl.json | 2 + app/javascript/mastodon/locales/no.json | 2 + app/javascript/mastodon/locales/oc.json | 2 + app/javascript/mastodon/locales/pl.json | 2 + app/javascript/mastodon/locales/pt-BR.json | 2 + app/javascript/mastodon/locales/pt.json | 2 + app/javascript/mastodon/locales/ru.json | 2 + app/javascript/mastodon/locales/th.json | 2 + app/javascript/mastodon/locales/tr.json | 2 + app/javascript/mastodon/locales/uk.json | 2 + app/javascript/mastodon/locales/zh-CN.json | 2 + app/javascript/mastodon/locales/zh-HK.json | 2 + app/javascript/mastodon/locales/zh-TW.json | 2 + app/javascript/mastodon/reducers/statuses.js | 4 ++ app/models/account.rb | 4 ++ app/models/concerns/account_interactions.rb | 4 ++ app/models/status.rb | 4 ++ app/models/status_pin.rb | 16 +++++ app/presenters/status_relationships_presenter.rb | 19 ++++-- app/serializers/rest/status_serializer.rb | 16 +++++ app/validators/status_pin_validator.rb | 9 +++ app/views/accounts/show.html.haml | 3 + app/views/stream_entries/_status.html.haml | 7 ++ config/locales/en.yml | 5 ++ config/routes.rb | 13 ++-- db/migrate/20170823162448_create_status_pins.rb | 10 +++ db/schema.rb | 12 +++- .../api/v1/accounts/statuses_controller_spec.rb | 36 +++++++--- .../api/v1/statuses/pins_controller_spec.rb | 57 ++++++++++++++++ spec/fabricators/status_pin_fabricator.rb | 4 ++ spec/models/status_pin_spec.rb | 41 ++++++++++++ 59 files changed, 493 insertions(+), 29 deletions(-) create mode 100644 app/controllers/api/v1/statuses/pins_controller.rb create mode 100644 app/models/status_pin.rb create mode 100644 app/validators/status_pin_validator.rb create mode 100644 db/migrate/20170823162448_create_status_pins.rb create mode 100644 spec/controllers/api/v1/statuses/pins_controller_spec.rb create mode 100644 spec/fabricators/status_pin_fabricator.rb create mode 100644 spec/models/status_pin_spec.rb (limited to 'app/controllers/api') diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index c6b98628e..f4ca239ba 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,14 +7,17 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do + @pinned_statuses = [] + if current_account && @account.blocking?(current_account) @statuses = [] return end - @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) - @statuses = cache_collection(@statuses, Status) - @next_url = next_url unless @statuses.empty? + @pinned_statuses = cache_collection(@account.pinned_statuses.limit(1), Status) unless media_requested? + @statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id]) + @statuses = cache_collection(@statuses, Status) + @next_url = next_url unless @statuses.empty? end format.atom do @@ -32,8 +35,8 @@ class AccountsController < ApplicationController def filtered_statuses default_statuses.tap do |statuses| - statuses.merge!(only_media_scope) if request.path.ends_with?('/media') - statuses.merge!(no_replies_scope) unless request.path.ends_with?('/with_replies') + statuses.merge!(only_media_scope) if media_requested? + statuses.merge!(no_replies_scope) unless replies_requested? end end @@ -58,12 +61,20 @@ class AccountsController < ApplicationController end def next_url - if request.path.ends_with?('/media') + if media_requested? short_account_media_url(@account, max_id: @statuses.last.id) - elsif request.path.ends_with?('/with_replies') + elsif replies_requested? short_account_with_replies_url(@account, max_id: @statuses.last.id) else short_account_url(@account, max_id: @statuses.last.id) end end + + def media_requested? + request.path.ends_with?('/media') + end + + def replies_requested? + request.path.ends_with?('/with_replies') + end end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index d9ae5c089..095f6937b 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -29,6 +29,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def account_statuses default_statuses.tap do |statuses| statuses.merge!(only_media_scope) if params[:only_media] + statuses.merge!(pinned_scope) if params[:pinned] statuses.merge!(no_replies_scope) if params[:exclude_replies] end end @@ -53,6 +54,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController @account.media_attachments.attached.reorder(nil).select(:status_id).distinct end + def pinned_scope + @account.pinned_statuses + end + def no_replies_scope Status.without_replies end diff --git a/app/controllers/api/v1/statuses/pins_controller.rb b/app/controllers/api/v1/statuses/pins_controller.rb new file mode 100644 index 000000000..3de1009b8 --- /dev/null +++ b/app/controllers/api/v1/statuses/pins_controller.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class Api::V1::Statuses::PinsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write } + before_action :require_user! + before_action :set_status + + respond_to :json + + def create + StatusPin.create!(account: current_account, status: @status) + render json: @status, serializer: REST::StatusSerializer + end + + def destroy + pin = StatusPin.find_by(account: current_account, status: @status) + pin&.destroy! + render json: @status, serializer: REST::StatusSerializer + end + + private + + def set_status + @status = Status.find(params[:status_id]) + end +end diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js index 36eec4934..7b5f4bd9c 100644 --- a/app/javascript/mastodon/actions/interactions.js +++ b/app/javascript/mastodon/actions/interactions.js @@ -24,6 +24,14 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; +export const PIN_REQUEST = 'PIN_REQUEST'; +export const PIN_SUCCESS = 'PIN_SUCCESS'; +export const PIN_FAIL = 'PIN_FAIL'; + +export const UNPIN_REQUEST = 'UNPIN_REQUEST'; +export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; +export const UNPIN_FAIL = 'UNPIN_FAIL'; + export function reblog(status) { return function (dispatch, getState) { dispatch(reblogRequest(status)); @@ -233,3 +241,73 @@ export function fetchFavouritesFail(id, error) { error, }; }; + +export function pin(status) { + return (dispatch, getState) => { + dispatch(pinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { + dispatch(pinSuccess(status, response.data)); + }).catch(error => { + dispatch(pinFail(status, error)); + }); + }; +}; + +export function pinRequest(status) { + return { + type: PIN_REQUEST, + status, + }; +}; + +export function pinSuccess(status, response) { + return { + type: PIN_SUCCESS, + status, + response, + }; +}; + +export function pinFail(status, error) { + return { + type: PIN_FAIL, + status, + error, + }; +}; + +export function unpin (status) { + return (dispatch, getState) => { + dispatch(unpinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { + dispatch(unpinSuccess(status, response.data)); + }).catch(error => { + dispatch(unpinFail(status, error)); + }); + }; +}; + +export function unpinRequest(status) { + return { + type: UNPIN_REQUEST, + status, + }; +}; + +export function unpinSuccess(status, response) { + return { + type: UNPIN_SUCCESS, + status, + response, + }; +}; + +export function unpinFail(status, error) { + return { + type: UNPIN_FAIL, + status, + error, + }; +}; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 38a4aafc1..b4f523f72 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -31,6 +31,7 @@ export default class Status extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onPin: PropTypes.func, onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, onBlock: PropTypes.func, diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 0d8c9add4..6436d6ebe 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -21,6 +21,8 @@ const messages = defineMessages({ report: { id: 'status.report', defaultMessage: 'Report @{name}' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, + unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, }); @injectIntl @@ -41,6 +43,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onBlock: PropTypes.func, onReport: PropTypes.func, onMuteConversation: PropTypes.func, + onPin: PropTypes.func, me: PropTypes.number, withDismiss: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -77,6 +80,10 @@ export default class StatusActionBar extends ImmutablePureComponent { this.props.onDelete(this.props.status); } + handlePinClick = () => { + this.props.onPin(this.props.status); + } + handleMentionClick = () => { this.props.onMention(this.props.status.get('account'), this.context.router.history); } @@ -121,6 +128,10 @@ export default class StatusActionBar extends ImmutablePureComponent { } if (status.getIn(['account', 'id']) === me) { + if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + } + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index b150165aa..c488b6ce7 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -11,6 +11,8 @@ import { favourite, unreblog, unfavourite, + pin, + unpin, } from '../actions/interactions'; import { blockAccount, @@ -72,6 +74,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onPin (status) { + if (status.get('pinned')) { + dispatch(unpin(status)); + } else { + dispatch(pin(status)); + } + }, + onDelete (status) { if (!this.deleteModal) { dispatch(deleteStatus(status.get('id'))); diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index 91ac64de2..c4a614677 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -14,6 +14,8 @@ const messages = defineMessages({ favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, report: { id: 'status.report', defaultMessage: 'Report @{name}' }, share: { id: 'status.share', defaultMessage: 'Share' }, + pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, + unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, }); @injectIntl @@ -31,6 +33,7 @@ export default class ActionBar extends React.PureComponent { onDelete: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onReport: PropTypes.func, + onPin: PropTypes.func, me: PropTypes.number.isRequired, intl: PropTypes.object.isRequired, }; @@ -59,6 +62,10 @@ export default class ActionBar extends React.PureComponent { this.props.onReport(this.props.status); } + handlePinClick = () => { + this.props.onPin(this.props.status); + } + handleShare = () => { navigator.share({ text: this.props.status.get('search_index'), @@ -72,6 +79,10 @@ export default class ActionBar extends React.PureComponent { let menu = []; if (me === status.getIn(['account', 'id'])) { + if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + } + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index cbabdd5bc..84e717a12 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -12,6 +12,8 @@ import { unfavourite, reblog, unreblog, + pin, + unpin, } from '../../actions/interactions'; import { replyCompose, @@ -87,6 +89,14 @@ export default class Status extends ImmutablePureComponent { } } + handlePin = (status) => { + if (status.get('pinned')) { + this.props.dispatch(unpin(status)); + } else { + this.props.dispatch(pin(status)); + } + } + handleReplyClick = (status) => { this.props.dispatch(replyCompose(status, this.context.router.history)); } @@ -187,6 +197,7 @@ export default class Status extends ImmutablePureComponent { onDelete={this.handleDeleteClick} onMention={this.handleMentionClick} onReport={this.handleReport} + onPin={this.handlePin} /> {descendants} diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index f5cf77f92..fa8cda97d 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -168,6 +168,7 @@ "status.mention": "أذكُر @{name}", "status.mute_conversation": "Mute conversation", "status.open": "وسع هذه المشاركة", + "status.pin": "Pin on profile", "status.reblog": "رَقِّي", "status.reblogged_by": "{name} رقى", "status.reply": "ردّ", @@ -179,6 +180,7 @@ "status.show_less": "إعرض أقلّ", "status.show_more": "أظهر المزيد", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "تحرير", "tabs_bar.federated_timeline": "الموحَّد", "tabs_bar.home": "الرئيسية", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index e6788f9eb..4aa097d31 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -168,6 +168,7 @@ "status.mention": "Споменаване", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Споделяне", "status.reblogged_by": "{name} сподели", "status.reply": "Отговор", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Съставяне", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Начало", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 95b3c60bf..d9cb7c7a3 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -168,6 +168,7 @@ "status.mention": "Esmentar @{name}", "status.mute_conversation": "Silenciar conversació", "status.open": "Ampliar aquest estat", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} ha retootejat", "status.reply": "Respondre", @@ -179,6 +180,7 @@ "status.show_less": "Mostra menys", "status.show_more": "Mostra més", "status.unmute_conversation": "Activar conversació", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compondre", "tabs_bar.federated_timeline": "Federada", "tabs_bar.home": "Inici", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 67a99b765..a5232552f 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -168,6 +168,7 @@ "status.mention": "Erwähnen", "status.mute_conversation": "Mute conversation", "status.open": "Öffnen", + "status.pin": "Pin on profile", "status.reblog": "Teilen", "status.reblogged_by": "{name} teilte", "status.reply": "Antworten", @@ -179,6 +180,7 @@ "status.show_less": "Weniger anzeigen", "status.show_more": "Mehr anzeigen", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Schreiben", "tabs_bar.federated_timeline": "Föderation", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index ef76f6e5b..fdb8aefe1 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -189,6 +189,14 @@ { "defaultMessage": "Unmute conversation", "id": "status.unmute_conversation" + }, + { + "defaultMessage": "Pin on profile", + "id": "status.pin" + }, + { + "defaultMessage": "Unpin from profile", + "id": "status.unpin" } ], "path": "app/javascript/mastodon/components/status_action_bar.json" @@ -1035,6 +1043,14 @@ { "defaultMessage": "Share", "id": "status.share" + }, + { + "defaultMessage": "Pin on profile", + "id": "status.pin" + }, + { + "defaultMessage": "Unpin from profile", + "id": "status.unpin" } ], "path": "app/javascript/mastodon/features/status/components/action_bar.json" diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 2ea2062d3..595063888 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -168,6 +168,7 @@ "status.mention": "Mention @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} boosted", "status.reply": "Reply", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 960d747ec..ed323f406 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -168,6 +168,7 @@ "status.mention": "Mencii @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Diskonigi", "status.reblogged_by": "{name} diskonigita", "status.reply": "Respondi", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Ekskribi", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Hejmo", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 212d16639..2fee29148 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar", "status.mute_conversation": "Mute conversation", "status.open": "Expandir estado", + "status.pin": "Pin on profile", "status.reblog": "Retoot", "status.reblogged_by": "Retooteado por {name}", "status.reply": "Responder", @@ -179,6 +180,7 @@ "status.show_less": "Mostrar menos", "status.show_more": "Mostrar más", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Redactar", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Inicio", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 5ada62f93..89fa014e4 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -168,6 +168,7 @@ "status.mention": "نام‌بردن از @{name}", "status.mute_conversation": "بی‌صداکردن گفتگو", "status.open": "این نوشته را باز کن", + "status.pin": "Pin on profile", "status.reblog": "بازبوقیدن", "status.reblogged_by": "‫{name}‬ بازبوقید", "status.reply": "پاسخ", @@ -179,6 +180,7 @@ "status.show_less": "نهفتن", "status.show_more": "نمایش", "status.unmute_conversation": "باصداکردن گفتگو", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "بنویسید", "tabs_bar.federated_timeline": "همگانی", "tabs_bar.home": "خانه", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index cb9e9c2a6..1c1334899 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -168,6 +168,7 @@ "status.mention": "Mainitse @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Buustaa", "status.reblogged_by": "{name} buustasi", "status.reply": "Vastaa", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Luo", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Koti", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 34a89a69f..479b8de7d 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -168,6 +168,7 @@ "status.mention": "Mentionner", "status.mute_conversation": "Masquer la conversation", "status.open": "Déplier ce statut", + "status.pin": "Pin on profile", "status.reblog": "Partager", "status.reblogged_by": "{name} a partagé :", "status.reply": "Répondre", @@ -179,6 +180,7 @@ "status.show_less": "Replier", "status.show_more": "Déplier", "status.unmute_conversation": "Ne plus masquer la conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Composer", "tabs_bar.federated_timeline": "Fil public global", "tabs_bar.home": "Accueil", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 34266d8e1..1e221af9c 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -168,6 +168,7 @@ "status.mention": "פניה אל @{name}", "status.mute_conversation": "השתקת שיחה", "status.open": "הרחבת הודעה", + "status.pin": "Pin on profile", "status.reblog": "הדהוד", "status.reblogged_by": "הודהד על ידי {name}", "status.reply": "תגובה", @@ -179,6 +180,7 @@ "status.show_less": "הראה פחות", "status.show_more": "הראה יותר", "status.unmute_conversation": "הסרת השתקת שיחה", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "חיבור", "tabs_bar.federated_timeline": "ציר זמן בין-קהילתי", "tabs_bar.home": "בבית", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index f69b096d4..2effecb1e 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -168,6 +168,7 @@ "status.mention": "Spomeni @{name}", "status.mute_conversation": "Utišaj razgovor", "status.open": "Proširi ovaj status", + "status.pin": "Pin on profile", "status.reblog": "Podigni", "status.reblogged_by": "{name} je podigao", "status.reply": "Odgovori", @@ -179,6 +180,7 @@ "status.show_less": "Pokaži manje", "status.show_more": "Pokaži više", "status.unmute_conversation": "Poništi utišavanje razgovora", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Sastavi", "tabs_bar.federated_timeline": "Federalni", "tabs_bar.home": "Dom", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 4d2a50963..59a7b8deb 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -168,6 +168,7 @@ "status.mention": "Említés", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Reblog", "status.reblogged_by": "{name} reblogolta", "status.reply": "Válasz", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Összeállítás", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Kezdőlap", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 532739e3c..9dd66b6cd 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -168,6 +168,7 @@ "status.mention": "Balasan @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Tampilkan status ini", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "di-boost {name}", "status.reply": "Balas", @@ -179,6 +180,7 @@ "status.show_less": "Tampilkan lebih sedikit", "status.show_more": "Tampilkan semua", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Tulis", "tabs_bar.federated_timeline": "Gabungan", "tabs_bar.home": "Beranda", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index a5e363e40..07184ae81 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Detaligar ca mesajo", + "status.pin": "Pin on profile", "status.reblog": "Repetar", "status.reblogged_by": "{name} repetita", "status.reply": "Respondar", @@ -179,6 +180,7 @@ "status.show_less": "Montrar mine", "status.show_more": "Montrar plue", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Kompozar", "tabs_bar.federated_timeline": "Federata", "tabs_bar.home": "Hemo", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 329eb82ca..369ae7f32 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -168,6 +168,7 @@ "status.mention": "Nomina @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Espandi questo post", + "status.pin": "Pin on profile", "status.reblog": "Condividi", "status.reblogged_by": "{name} ha condiviso", "status.reply": "Rispondi", @@ -179,6 +180,7 @@ "status.show_less": "Mostra meno", "status.show_more": "Mostra di più", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Scrivi", "tabs_bar.federated_timeline": "Federazione", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index 757190c90..c35b0def3 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -168,6 +168,7 @@ "status.mention": "返信", "status.mute_conversation": "会話をミュート", "status.open": "詳細を表示", + "status.pin": "Pin on profile", "status.reblog": "ブースト", "status.reblogged_by": "{name}さんにブーストされました", "status.reply": "返信", @@ -179,6 +180,7 @@ "status.show_less": "隠す", "status.show_more": "もっと見る", "status.unmute_conversation": "会話のミュートを解除", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "投稿", "tabs_bar.federated_timeline": "連合", "tabs_bar.home": "ホーム", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 47d0d4087..52ba1e70f 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -168,6 +168,7 @@ "status.mention": "답장", "status.mute_conversation": "이 대화를 뮤트", "status.open": "상세 정보 표시", + "status.pin": "Pin on profile", "status.reblog": "부스트", "status.reblogged_by": "{name}님이 부스트 했습니다", "status.reply": "답장", @@ -179,6 +180,7 @@ "status.show_less": "숨기기", "status.show_more": "더 보기", "status.unmute_conversation": "이 대화의 뮤트 해제하기", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "포스트", "tabs_bar.federated_timeline": "연합", "tabs_bar.home": "홈", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 4d68c7992..fb4127831 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -168,6 +168,7 @@ "status.mention": "Vermeld @{name}", "status.mute_conversation": "Negeer conversatie", "status.open": "Toot volledig tonen", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} boostte", "status.reply": "Reageren", @@ -179,6 +180,7 @@ "status.show_less": "Minder tonen", "status.show_more": "Meer tonen", "status.unmute_conversation": "Conversatie niet meer negeren", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Schrijven", "tabs_bar.federated_timeline": "Globaal", "tabs_bar.home": "Start", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 9453e65ff..2d6224c48 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -168,6 +168,7 @@ "status.mention": "Nevn @{name}", "status.mute_conversation": "Demp samtale", "status.open": "Utvid denne statusen", + "status.pin": "Pin on profile", "status.reblog": "Fremhev", "status.reblogged_by": "Fremhevd av {name}", "status.reply": "Svar", @@ -179,6 +180,7 @@ "status.show_less": "Vis mindre", "status.show_more": "Vis mer", "status.unmute_conversation": "Ikke demp samtale", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Komponer", "tabs_bar.federated_timeline": "Felles", "tabs_bar.home": "Hjem", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 5e5e28af0..34e1a8c47 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar", "status.mute_conversation": "Rescondre la conversacion", "status.open": "Desplegar aqueste estatut", + "status.pin": "Pin on profile", "status.reblog": "Partejar", "status.reblogged_by": "{name} a partejat :", "status.reply": "Respondre", @@ -179,6 +180,7 @@ "status.show_less": "Tornar plegar", "status.show_more": "Desplegar", "status.unmute_conversation": "Conversacions amb silenci levat", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compausar", "tabs_bar.federated_timeline": "Flux public global", "tabs_bar.home": "Acuèlh", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index af38bbb6c..8a8d0f38a 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -168,6 +168,7 @@ "status.mention": "Wspomnij o @{name}", "status.mute_conversation": "Wycisz konwersację", "status.open": "Rozszerz ten status", + "status.pin": "Pin on profile", "status.reblog": "Podbij", "status.reblogged_by": "{name} podbił", "status.reply": "Odpowiedz", @@ -179,6 +180,7 @@ "status.show_less": "Pokaż mniej", "status.show_more": "Pokaż więcej", "status.unmute_conversation": "Cofnij wyciszenie konwersacji", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Napisz", "tabs_bar.federated_timeline": "Globalne", "tabs_bar.home": "Strona główna", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 55d2f05de..8a299e272 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expandir", + "status.pin": "Pin on profile", "status.reblog": "Partilhar", "status.reblogged_by": "{name} partilhou", "status.reply": "Responder", @@ -179,6 +180,7 @@ "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 55d2f05de..8a299e272 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -168,6 +168,7 @@ "status.mention": "Mencionar @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expandir", + "status.pin": "Pin on profile", "status.reblog": "Partilhar", "status.reblogged_by": "{name} partilhou", "status.reply": "Responder", @@ -179,6 +180,7 @@ "status.show_less": "Mostrar menos", "status.show_more": "Mostrar mais", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Criar", "tabs_bar.federated_timeline": "Global", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index af38fc723..822f116c7 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -168,6 +168,7 @@ "status.mention": "Упомянуть @{name}", "status.mute_conversation": "Заглушить тред", "status.open": "Развернуть статус", + "status.pin": "Pin on profile", "status.reblog": "Продвинуть", "status.reblogged_by": "{name} продвинул(а)", "status.reply": "Ответить", @@ -179,6 +180,7 @@ "status.show_less": "Свернуть", "status.show_more": "Развернуть", "status.unmute_conversation": "Снять глушение с треда", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Написать", "tabs_bar.federated_timeline": "Глобальная", "tabs_bar.home": "Главная", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index aa0929f82..9c985eec9 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -168,6 +168,7 @@ "status.mention": "Mention @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Expand this status", + "status.pin": "Pin on profile", "status.reblog": "Boost", "status.reblogged_by": "{name} boosted", "status.reply": "Reply", @@ -179,6 +180,7 @@ "status.show_less": "Show less", "status.show_more": "Show more", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Compose", "tabs_bar.federated_timeline": "Federated", "tabs_bar.home": "Home", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 37ce8597e..41c9d44a7 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -168,6 +168,7 @@ "status.mention": "Bahset @{name}", "status.mute_conversation": "Mute conversation", "status.open": "Bu gönderiyi genişlet", + "status.pin": "Pin on profile", "status.reblog": "Boost'la", "status.reblogged_by": "{name} boost etti", "status.reply": "Cevapla", @@ -179,6 +180,7 @@ "status.show_less": "Daha azı", "status.show_more": "Daha fazlası", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Oluştur", "tabs_bar.federated_timeline": "Federe", "tabs_bar.home": "Ana sayfa", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index fea7bd94e..6087e3a1e 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -168,6 +168,7 @@ "status.mention": "Згадати", "status.mute_conversation": "Заглушити діалог", "status.open": "Розгорнути допис", + "status.pin": "Pin on profile", "status.reblog": "Передмухнути", "status.reblogged_by": "{name} передмухнув(-ла)", "status.reply": "Відповісти", @@ -179,6 +180,7 @@ "status.show_less": "Згорнути", "status.show_more": "Розгорнути", "status.unmute_conversation": "Зняти глушення з діалогу", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "Написати", "tabs_bar.federated_timeline": "Глобальна", "tabs_bar.home": "Головна", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index e7c431454..2e3b4b0b8 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -168,6 +168,7 @@ "status.mention": "提及 @{name}", "status.mute_conversation": "Mute conversation", "status.open": "展开嘟文", + "status.pin": "Pin on profile", "status.reblog": "转嘟", "status.reblogged_by": "{name} 转嘟", "status.reply": "回应", @@ -179,6 +180,7 @@ "status.show_less": "减少显示", "status.show_more": "显示更多", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "撰写", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主页", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index 7312aae82..1ab3b3f9d 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -168,6 +168,7 @@ "status.mention": "提及 @{name}", "status.mute_conversation": "Mute conversation", "status.open": "展開文章", + "status.pin": "Pin on profile", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推", "status.reply": "回應", @@ -179,6 +180,7 @@ "status.show_less": "減少顯示", "status.show_more": "顯示更多", "status.unmute_conversation": "Unmute conversation", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "撰寫", "tabs_bar.federated_timeline": "跨站", "tabs_bar.home": "主頁", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 1c2e35272..571a2383d 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -168,6 +168,7 @@ "status.mention": "提到 @{name}", "status.mute_conversation": "消音對話", "status.open": "展開這個狀態", + "status.pin": "Pin on profile", "status.reblog": "轉推", "status.reblogged_by": "{name} 轉推了", "status.reply": "回應", @@ -179,6 +180,7 @@ "status.show_less": "看少點", "status.show_more": "看更多", "status.unmute_conversation": "不消音對話", + "status.unpin": "Unpin from profile", "tabs_bar.compose": "編輯", "tabs_bar.federated_timeline": "聯盟", "tabs_bar.home": "家", diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 3e40b0b42..38691dc43 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -7,6 +7,8 @@ import { FAVOURITE_SUCCESS, FAVOURITE_FAIL, UNFAVOURITE_SUCCESS, + PIN_SUCCESS, + UNPIN_SUCCESS, } from '../actions/interactions'; import { STATUS_FETCH_SUCCESS, @@ -114,6 +116,8 @@ export default function statuses(state = initialState, action) { case UNREBLOG_SUCCESS: case FAVOURITE_SUCCESS: case UNFAVOURITE_SUCCESS: + case PIN_SUCCESS: + case UNPIN_SUCCESS: return normalizeStatus(state, action.response); case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); diff --git a/app/models/account.rb b/app/models/account.rb index 0c9c6aed4..b83aa1159 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -77,6 +77,10 @@ class Account < ApplicationRecord has_many :mentions, inverse_of: :account, dependent: :destroy has_many :notifications, inverse_of: :account, dependent: :destroy + # Pinned statuses + has_many :status_pins, inverse_of: :account, dependent: :destroy + has_many :pinned_statuses, through: :status_pins, class_name: 'Status', source: :status + # Media has_many :media_attachments, dependent: :destroy diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 9ffed2910..b26520f5b 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -138,4 +138,8 @@ module AccountInteractions def reblogged?(status) status.proper.reblogs.where(account: self).exists? end + + def pinned?(status) + status_pins.where(status: status).exists? + end end diff --git a/app/models/status.rb b/app/models/status.rb index 24eaf7071..3dc83ad1f 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -164,6 +164,10 @@ class Status < ApplicationRecord ConversationMute.select('conversation_id').where(conversation_id: conversation_ids).where(account_id: account_id).map { |m| [m.conversation_id, true] }.to_h end + def pins_map(status_ids, account_id) + StatusPin.select('status_id').where(status_id: status_ids).where(account_id: account_id).map { |p| [p.status_id, true] }.to_h + end + def reload_stale_associations!(cached_items) account_ids = [] diff --git a/app/models/status_pin.rb b/app/models/status_pin.rb new file mode 100644 index 000000000..c9a669344 --- /dev/null +++ b/app/models/status_pin.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +# == Schema Information +# +# Table name: status_pins +# +# id :integer not null, primary key +# account_id :integer not null +# status_id :integer not null +# + +class StatusPin < ApplicationRecord + belongs_to :account, required: true + belongs_to :status, required: true + + validates_with StatusPinValidator +end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index 03294015f..10b449504 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -1,19 +1,24 @@ # frozen_string_literal: true class StatusRelationshipsPresenter - attr_reader :reblogs_map, :favourites_map, :mutes_map + attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map - def initialize(statuses, current_account_id = nil, reblogs_map: {}, favourites_map: {}, mutes_map: {}) + def initialize(statuses, current_account_id = nil, options = {}) if current_account_id.nil? @reblogs_map = {} @favourites_map = {} @mutes_map = {} + @pins_map = {} else - status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq - conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq - @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(reblogs_map) - @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(favourites_map) - @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(mutes_map) + statuses = statuses.compact + status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq + conversation_ids = statuses.map(&:conversation_id).compact.uniq + pinnable_status_ids = statuses.map(&:proper).select { |s| s.account_id == current_account_id && %w(public unlisted).include?(s.visibility) }.map(&:id) + + @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) + @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {}) + @mutes_map = Status.mutes_map(conversation_ids, current_account_id).merge(options[:mutes_map] || {}) + @pins_map = Status.pins_map(pinnable_status_ids, current_account_id).merge(options[:pins_map] || {}) end end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 246b12a90..298a3bb40 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -8,6 +8,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? attribute :muted, if: :current_user? + attribute :pinned, if: :pinnable? belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application @@ -57,6 +58,21 @@ class REST::StatusSerializer < ActiveModel::Serializer end end + def pinned + if instance_options && instance_options[:relationships] + instance_options[:relationships].pins_map[object.id] || false + else + current_user.account.pinned?(object) + end + end + + def pinnable? + current_user? && + current_user.account_id == object.account_id && + !object.reblog? && + %w(public unlisted).include?(object.visibility) + end + class ApplicationSerializer < ActiveModel::Serializer attributes :name, :website end diff --git a/app/validators/status_pin_validator.rb b/app/validators/status_pin_validator.rb new file mode 100644 index 000000000..f557df6af --- /dev/null +++ b/app/validators/status_pin_validator.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class StatusPinValidator < ActiveModel::Validator + def validate(pin) + pin.errors.add(:status, I18n.t('statuses.pin_errors.reblog')) if pin.status.reblog? + pin.errors.add(:status, I18n.t('statuses.pin_errors.ownership')) if pin.account_id != pin.status.account_id + pin.errors.add(:status, I18n.t('statuses.pin_errors.private')) unless %w(public unlisted).include?(pin.status.visibility) + end +end diff --git a/app/views/accounts/show.html.haml b/app/views/accounts/show.html.haml index ec44f4c74..e0f9f869a 100644 --- a/app/views/accounts/show.html.haml +++ b/app/views/accounts/show.html.haml @@ -30,6 +30,9 @@ = render 'nothing_here' - else .activity-stream.with-header + - if params[:page].to_i.zero? + = render partial: 'stream_entries/status', collection: @pinned_statuses, as: :status, locals: { pinned: true } + = render partial: 'stream_entries/status', collection: @statuses, as: :status - if @statuses.size == 20 diff --git a/app/views/stream_entries/_status.html.haml b/app/views/stream_entries/_status.html.haml index 50a373743..e2e1fdd12 100644 --- a/app/views/stream_entries/_status.html.haml +++ b/app/views/stream_entries/_status.html.haml @@ -1,4 +1,5 @@ :ruby + pinned ||= false include_threads ||= false is_predecessor ||= false is_successor ||= false @@ -25,6 +26,12 @@ = link_to TagManager.instance.url_for(status.account), class: 'status__display-name muted' do %strong.emojify= display_name(status.account) = t('stream_entries.reblogged') + - elsif pinned + .pre-header + .pre-header__icon + = fa_icon('thumb-tack fw') + %span + = t('stream_entries.pinned') = render (centered ? 'stream_entries/detailed_status' : 'stream_entries/simple_status'), status: status.proper diff --git a/config/locales/en.yml b/config/locales/en.yml index 97bb14186..96d08e6b2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -434,6 +434,10 @@ en: statuses: open_in_web: Open in web over_character_limit: character limit of %{max} exceeded + pin_errors: + ownership: Someone else's toot cannot be pinned + private: Non-public toot cannot be pinned + reblog: A boost cannot be pinned show_more: Show more visibilities: private: Followers-only @@ -444,6 +448,7 @@ en: unlisted_long: Everyone can see, but not listed on public timelines stream_entries: click_to_show: Click to show + pinned: Pinned toot reblogged: boosted sensitive_content: Sensitive content terms: diff --git a/config/routes.rb b/config/routes.rb index 94a4ac88e..7588805c0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -162,6 +162,9 @@ Rails.application.routes.draw do resource :mute, only: :create post :unmute, to: 'mutes#destroy' + + resource :pin, only: :create + post :unpin, to: 'pins#destroy' end member do @@ -175,7 +178,8 @@ Rails.application.routes.draw do resource :public, only: :show, controller: :public resources :tag, only: :show end - resources :streaming, only: [:index] + + resources :streaming, only: [:index] get '/search', to: 'search#index', as: :search @@ -210,6 +214,7 @@ Rails.application.routes.draw do resource :search, only: :show, controller: :search resources :relationships, only: :index end + resources :accounts, only: [:show] do resources :statuses, only: :index, controller: 'accounts/statuses' resources :followers, only: :index, controller: 'accounts/follower_accounts' @@ -245,7 +250,7 @@ Rails.application.routes.draw do root 'home#index' match '*unmatched_route', - via: :all, - to: 'application#raise_not_found', - format: false + via: :all, + to: 'application#raise_not_found', + format: false end diff --git a/db/migrate/20170823162448_create_status_pins.rb b/db/migrate/20170823162448_create_status_pins.rb new file mode 100644 index 000000000..9a6d4a7b9 --- /dev/null +++ b/db/migrate/20170823162448_create_status_pins.rb @@ -0,0 +1,10 @@ +class CreateStatusPins < ActiveRecord::Migration[5.1] + def change + create_table :status_pins do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false + t.belongs_to :status, foreign_key: { on_delete: :cascade }, null: false + end + + add_index :status_pins, [:account_id, :status_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 98b07e282..d0e72be0f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170720000000) do +ActiveRecord::Schema.define(version: 20170823162448) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -282,6 +282,14 @@ ActiveRecord::Schema.define(version: 20170720000000) do t.index ["thing_type", "thing_id", "var"], name: "index_settings_on_thing_type_and_thing_id_and_var", unique: true end + create_table "status_pins", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "status_id", null: false + t.index ["account_id", "status_id"], name: "index_status_pins_on_account_id_and_status_id", unique: true + t.index ["account_id"], name: "index_status_pins_on_account_id" + t.index ["status_id"], name: "index_status_pins_on_status_id" + end + create_table "statuses", force: :cascade do |t| t.string "uri" t.integer "account_id", null: false @@ -430,6 +438,8 @@ ActiveRecord::Schema.define(version: 20170720000000) do add_foreign_key "reports", "accounts", on_delete: :cascade add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade add_foreign_key "session_activations", "users", on_delete: :cascade + add_foreign_key "status_pins", "accounts", on_delete: :cascade + add_foreign_key "status_pins", "statuses", on_delete: :cascade add_foreign_key "statuses", "accounts", column: "in_reply_to_account_id", on_delete: :nullify add_foreign_key "statuses", "accounts", on_delete: :cascade add_foreign_key "statuses", "statuses", column: "in_reply_to_id", on_delete: :nullify diff --git a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb index 8b4fd6a5b..c49a77ac3 100644 --- a/spec/controllers/api/v1/accounts/statuses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/statuses_controller_spec.rb @@ -18,21 +18,37 @@ describe Api::V1::Accounts::StatusesController do expect(response).to have_http_status(:success) expect(response.headers['Link'].links.size).to eq(2) end - end - describe 'GET #index with only media' do - it 'returns http success' do - get :index, params: { account_id: user.account.id, only_media: true } + context 'with only media' do + it 'returns http success' do + get :index, params: { account_id: user.account.id, only_media: true } - expect(response).to have_http_status(:success) + expect(response).to have_http_status(:success) + end end - end - describe 'GET #index with exclude replies' do - it 'returns http success' do - get :index, params: { account_id: user.account.id, exclude_replies: true } + context 'with exclude replies' do + before do + Fabricate(:status, account: user.account, thread: Fabricate(:status)) + end - expect(response).to have_http_status(:success) + it 'returns http success' do + get :index, params: { account_id: user.account.id, exclude_replies: true } + + expect(response).to have_http_status(:success) + end + end + + context 'with only pinned' do + before do + Fabricate(:status_pin, account: user.account, status: Fabricate(:status, account: user.account)) + end + + it 'returns http success' do + get :index, params: { account_id: user.account.id, pinned: true } + + expect(response).to have_http_status(:success) + end end end end diff --git a/spec/controllers/api/v1/statuses/pins_controller_spec.rb b/spec/controllers/api/v1/statuses/pins_controller_spec.rb new file mode 100644 index 000000000..2e170da24 --- /dev/null +++ b/spec/controllers/api/v1/statuses/pins_controller_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Statuses::PinsController do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'write', application: app) } + + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'POST #create' do + let(:status) { Fabricate(:status, account: user.account) } + + before do + post :create, params: { status_id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates the pinned attribute' do + expect(user.account.pinned?(status)).to be true + end + + it 'return json with updated attributes' do + hash_body = body_as_json + + expect(hash_body[:id]).to eq status.id + expect(hash_body[:pinned]).to be true + end + end + + describe 'POST #destroy' do + let(:status) { Fabricate(:status, account: user.account) } + + before do + Fabricate(:status_pin, status: status, account: user.account) + post :destroy, params: { status_id: status.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates the pinned attribute' do + expect(user.account.pinned?(status)).to be false + end + end + end +end diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb new file mode 100644 index 000000000..6a9006c9f --- /dev/null +++ b/spec/fabricators/status_pin_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:status_pin) do + account + status +end diff --git a/spec/models/status_pin_spec.rb b/spec/models/status_pin_spec.rb new file mode 100644 index 000000000..6f54f80f9 --- /dev/null +++ b/spec/models/status_pin_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe StatusPin, type: :model do + describe 'validations' do + it 'allows pins of own statuses' do + account = Fabricate(:account) + status = Fabricate(:status, account: account) + + expect(StatusPin.new(account: account, status: status).save).to be true + end + + it 'does not allow pins of statuses by someone else' do + account = Fabricate(:account) + status = Fabricate(:status) + + expect(StatusPin.new(account: account, status: status).save).to be false + end + + it 'does not allow pins of reblogs' do + account = Fabricate(:account) + status = Fabricate(:status, account: account) + reblog = Fabricate(:status, reblog: status) + + expect(StatusPin.new(account: account, status: reblog).save).to be false + end + + it 'does not allow pins of private statuses' do + account = Fabricate(:account) + status = Fabricate(:status, account: account, visibility: :private) + + expect(StatusPin.new(account: account, status: status).save).to be false + end + + it 'does not allow pins of direct statuses' do + account = Fabricate(:account) + status = Fabricate(:status, account: account, visibility: :direct) + + expect(StatusPin.new(account: account, status: status).save).to be false + end + end +end -- cgit From 649a20ab46eadf9ae3bfc30782ae62379383bd72 Mon Sep 17 00:00:00 2001 From: masarakki Date: Sat, 26 Aug 2017 19:40:03 +0900 Subject: authorize-follow-requests-after-unlocking (#4658) --- .../api/v1/accounts/credentials_controller.rb | 2 +- app/controllers/settings/profiles_controller.rb | 2 +- app/services/update_account_service.rb | 21 +++++++++++++++++++++ app/workers/authorize_follow_worker.rb | 14 ++++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 app/services/update_account_service.rb create mode 100644 app/workers/authorize_follow_worker.rb (limited to 'app/controllers/api') diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index bea83cd2a..da534d960 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def update @account = current_account - @account.update!(account_params) + UpdateAccountService.new.call(@account, account_params, raise_error: true) ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index c751c64ae..28f78a4fb 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -14,7 +14,7 @@ class Settings::ProfilesController < ApplicationController def show; end def update - if @account.update(account_params) + if UpdateAccountService.new.call(@account, account_params) ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb new file mode 100644 index 000000000..09ea377e7 --- /dev/null +++ b/app/services/update_account_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class UpdateAccountService < BaseService + def call(account, params, raise_error: false) + was_locked = account.locked + update_method = raise_error ? :update! : :update + account.send(update_method, params).tap do |ret| + next unless ret + authorize_all_follow_requests(account) if was_locked && !account.locked + end + end + + private + + def authorize_all_follow_requests(account) + follow_requests = FollowRequest.where(target_account: account) + AuthorizeFollowWorker.push_bulk(follow_requests) do |req| + [req.account_id, req.target_account_id] + end + end +end diff --git a/app/workers/authorize_follow_worker.rb b/app/workers/authorize_follow_worker.rb new file mode 100644 index 000000000..0d5014624 --- /dev/null +++ b/app/workers/authorize_follow_worker.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class AuthorizeFollowWorker + include Sidekiq::Worker + + def perform(source_account_id, target_account_id) + source_account = Account.find(source_account_id) + target_account = Account.find(target_account_id) + + AuthorizeFollowService.new.call(source_account, target_account) + rescue ActiveRecord::RecordNotFound + true + end +end -- cgit From e95bdec7c5da63930fc2e08e67e4358fec19296d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 30 Aug 2017 10:23:43 +0200 Subject: Update status embeds (#4742) - Use statuses controller for embeds instead of stream entries controller - Prefer /@:username/:id/embed URL for embeds - Use /@:username as author_url in OEmbed - Add follow link to embeds which opens web intent in new window - Use redis cache in development - Cache entire embed --- app/controllers/api/oembed_controller.rb | 8 ++-- app/controllers/statuses_controller.rb | 5 ++ app/controllers/stream_entries_controller.rb | 5 +- app/helpers/stream_entries_helper.rb | 2 +- app/javascript/packs/public.js | 7 +++ app/javascript/styles/stream_entries.scss | 30 ++++++++++++ app/lib/status_finder.rb | 34 +++++++++++++ app/lib/stream_entry_finder.rb | 34 ------------- app/serializers/oembed_serializer.rb | 4 +- .../stream_entries/_detailed_status.html.haml | 5 ++ app/views/stream_entries/embed.html.haml | 5 +- config/brakeman.ignore | 50 ++++++++++---------- config/environments/development.rb | 5 +- config/routes.rb | 2 + spec/controllers/stream_entries_controller_spec.rb | 6 +-- spec/lib/status_finder_spec.rb | 55 ++++++++++++++++++++++ spec/lib/stream_entry_finder_spec.rb | 55 ---------------------- 17 files changed, 179 insertions(+), 133 deletions(-) create mode 100644 app/lib/status_finder.rb delete mode 100644 app/lib/stream_entry_finder.rb create mode 100644 spec/lib/status_finder_spec.rb delete mode 100644 spec/lib/stream_entry_finder_spec.rb (limited to 'app/controllers/api') diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index f8c87dd16..37a163cd3 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -4,14 +4,14 @@ class Api::OEmbedController < Api::BaseController respond_to :json def show - @stream_entry = find_stream_entry.stream_entry - render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default + @status = status_finder.status + render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default end private - def find_stream_entry - StreamEntryFinder.new(params[:url]) + def status_finder + StatusFinder.new(params[:url]) end def maxwidth_or_default diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index a9768d092..65206ea96 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -30,6 +30,11 @@ class StatusesController < ApplicationController render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end + def embed + response.headers['X-Frame-Options'] = 'ALLOWALL' + render 'stream_entries/embed', layout: 'embedded' + end + private def set_account diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index ccb15495e..cc579dbc8 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -25,10 +25,7 @@ class StreamEntriesController < ApplicationController end def embed - response.headers['X-Frame-Options'] = 'ALLOWALL' - return gone if @stream_entry.activity.nil? - - render layout: 'embedded' + redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301 end private diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index 4ef7cffb0..445114985 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module StreamEntriesHelper - EMBEDDED_CONTROLLER = 'stream_entries' + EMBEDDED_CONTROLLER = 'statuses' EMBEDDED_ACTION = 'embed' def display_name(account) diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index d8a0f4eee..ce12041e6 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -38,6 +38,13 @@ function main() { content.title = dateTimeFormat.format(datetime); content.textContent = relativeFormat.format(datetime); }); + + [].forEach.call(document.querySelectorAll('.logo-button'), (content) => { + content.addEventListener('click', (e) => { + e.preventDefault(); + window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); + }); + }); }); delegate(document, '.video-player video', 'click', ({ target }) => { diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index 1192e2a80..7048ab110 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -421,3 +421,33 @@ } } } + +.button.button-secondary.logo-button { + position: absolute; + right: 14px; + top: 14px; + font-size: 14px; + + svg { + width: 20px; + height: auto; + vertical-align: middle; + margin-right: 5px; + + path:first-child { + fill: $ui-primary-color; + } + + path:last-child { + fill: $simple-background-color; + } + } + + &:active, + &:focus, + &:hover { + svg path:first-child { + fill: lighten($ui-primary-color, 4%); + } + } +} diff --git a/app/lib/status_finder.rb b/app/lib/status_finder.rb new file mode 100644 index 000000000..bd910f12b --- /dev/null +++ b/app/lib/status_finder.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class StatusFinder + attr_reader :url + + def initialize(url) + @url = url + end + + def status + verify_action! + + case recognized_params[:controller] + when 'stream_entries' + StreamEntry.find(recognized_params[:id]).status + when 'statuses' + Status.find(recognized_params[:id]) + else + raise ActiveRecord::RecordNotFound + end + end + + private + + def recognized_params + Rails.application.routes.recognize_path(url) + end + + def verify_action! + unless recognized_params[:action] == 'show' + raise ActiveRecord::RecordNotFound + end + end +end diff --git a/app/lib/stream_entry_finder.rb b/app/lib/stream_entry_finder.rb deleted file mode 100644 index 0ea33229c..000000000 --- a/app/lib/stream_entry_finder.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -class StreamEntryFinder - attr_reader :url - - def initialize(url) - @url = url - end - - def stream_entry - verify_action! - - case recognized_params[:controller] - when 'stream_entries' - StreamEntry.find(recognized_params[:id]) - when 'statuses' - Status.find(recognized_params[:id]).stream_entry - else - raise ActiveRecord::RecordNotFound - end - end - - private - - def recognized_params - Rails.application.routes.recognize_path(url) - end - - def verify_action! - unless recognized_params[:action] == 'show' - raise ActiveRecord::RecordNotFound - end - end -end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb index 78376d253..0c2ced859 100644 --- a/app/serializers/oembed_serializer.rb +++ b/app/serializers/oembed_serializer.rb @@ -21,7 +21,7 @@ class OEmbedSerializer < ActiveModel::Serializer end def author_url - account_url(object.account) + short_account_url(object.account) end def provider_name @@ -38,7 +38,7 @@ class OEmbedSerializer < ActiveModel::Serializer def html tag :iframe, - src: embed_account_stream_entry_url(object.account, object), + src: embed_short_account_status_url(object.account, object), style: 'width: 100%; overflow: hidden', frameborder: '0', scrolling: 'no', diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index 193cc6470..107202b75 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -1,4 +1,9 @@ .detailed-status.light + - if embedded_view? + = link_to "web+mastodon://follow?uri=#{status.account.local_username_and_domain}", class: 'button button-secondary logo-button', target: '_new' do + = render file: Rails.root.join('app', 'javascript', 'images', 'logo.svg') + = t('accounts.follow') + = link_to TagManager.instance.url_for(status.account), class: 'detailed-status__display-name p-author h-card', target: stream_link_target, rel: 'noopener' do %div .avatar diff --git a/app/views/stream_entries/embed.html.haml b/app/views/stream_entries/embed.html.haml index 5df82528b..b703c15d2 100644 --- a/app/views/stream_entries/embed.html.haml +++ b/app/views/stream_entries/embed.html.haml @@ -1,2 +1,3 @@ -.activity-stream.activity-stream-headless - = render @type, @type.to_sym => @stream_entry.activity, centered: true +- cache @stream_entry.activity do + .activity-stream.activity-stream-headless + = render "stream_entries/#{@type}", @type.to_sym => @stream_entry.activity, centered: true diff --git a/config/brakeman.ignore b/config/brakeman.ignore index f9bc77069..dbb59dd07 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -1,5 +1,24 @@ { "ignored_warnings": [ + { + "warning_type": "Dynamic Render Path", + "warning_code": 15, + "fingerprint": "44d3f14e05d8fbb5b23e13ac02f15aa38b2a2f0f03b9ba76bab7f98e155a4a4e", + "check_name": "Render", + "message": "Render path contains parameter value", + "file": "app/views/stream_entries/embed.html.haml", + "line": 3, + "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", + "code": "render(action => \"stream_entries/#{Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase}\", { Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity, :centered => true })", + "render_path": [{"type":"controller","class":"StatusesController","method":"embed","line":35,"file":"app/controllers/statuses_controller.rb"}], + "location": { + "type": "template", + "template": "stream_entries/embed" + }, + "user_input": "params[:id]", + "confidence": "Weak", + "note": "" + }, { "warning_type": "Dynamic Render Path", "warning_code": 15, @@ -7,10 +26,10 @@ "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/admin/accounts/index.html.haml", - "line": 32, + "line": 63, "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "code": "render(action => filtered_accounts.page(params[:page]), {})", - "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":7,"file":"app/controllers/admin/accounts_controller.rb"}], + "render_path": [{"type":"controller","class":"Admin::AccountsController","method":"index","line":10,"file":"app/controllers/admin/accounts_controller.rb"}], "location": { "type": "template", "template": "admin/accounts/index" @@ -39,25 +58,6 @@ "confidence": "High", "note": "" }, - { - "warning_type": "Dynamic Render Path", - "warning_code": 15, - "fingerprint": "c417f9d44ab05dd9cf3d5ec9df2324a5036774c151181787b32c4c940623191b", - "check_name": "Render", - "message": "Render path contains parameter value", - "file": "app/views/stream_entries/embed.html.haml", - "line": 2, - "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", - "code": "render(action => Account.find_local!(params[:account_username]).stream_entries.where(:activity_type => \"Status\").find(params[:id]).activity_type.downcase, { Account.find_local!(params[:account_username]).stream_entries.where(:activity_type => \"Status\").find(params[:id]).activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).stream_entries.where(:activity_type => \"Status\").find(params[:id]).activity, :centered => true })", - "render_path": [{"type":"controller","class":"StreamEntriesController","method":"embed","line":32,"file":"app/controllers/stream_entries_controller.rb"}], - "location": { - "type": "template", - "template": "stream_entries/embed" - }, - "user_input": "params[:id]", - "confidence": "Weak", - "note": "" - }, { "warning_type": "Dynamic Render Path", "warning_code": 15, @@ -84,10 +84,10 @@ "check_name": "Render", "message": "Render path contains parameter value", "file": "app/views/stream_entries/show.html.haml", - "line": 19, + "line": 23, "link": "http://brakemanscanner.org/docs/warning_types/dynamic_render_path/", "code": "render(partial => \"stream_entries/#{Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase}\", { :locals => ({ Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity_type.downcase.to_sym => Account.find_local!(params[:account_username]).statuses.find(params[:id]).stream_entry.activity, :include_threads => true }) })", - "render_path": [{"type":"controller","class":"StatusesController","method":"show","line":15,"file":"app/controllers/statuses_controller.rb"}], + "render_path": [{"type":"controller","class":"StatusesController","method":"show","line":20,"file":"app/controllers/statuses_controller.rb"}], "location": { "type": "template", "template": "stream_entries/show" @@ -97,6 +97,6 @@ "note": "" } ], - "updated": "2017-05-07 08:26:06 +0900", - "brakeman_version": "3.6.1" + "updated": "2017-08-30 05:14:04 +0200", + "brakeman_version": "3.7.2" } diff --git a/config/environments/development.rb b/config/environments/development.rb index 4c60965c8..59bc2c3e2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,9 +16,10 @@ Rails.application.configure do if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true - config.cache_store = :memory_store + config.cache_store = :redis_store, ENV['REDIS_URL'], REDIS_CACHE_PARAMS + config.public_file_server.headers = { - 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" + 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}", } else config.action_controller.perform_caching = false diff --git a/config/routes.rb b/config/routes.rb index 7588805c0..f8f145e1d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,6 +44,7 @@ Rails.application.routes.draw do resources :statuses, only: [:show] do member do get :activity + get :embed end end @@ -59,6 +60,7 @@ Rails.application.routes.draw do get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies get '/@:username/media', to: 'accounts#show', as: :short_account_media get '/@:account_username/:id', to: 'statuses#show', as: :short_account_status + get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status namespace :settings do resource :profile, only: [:show, :update] diff --git a/spec/controllers/stream_entries_controller_spec.rb b/spec/controllers/stream_entries_controller_spec.rb index 808cf667c..f81e2be7b 100644 --- a/spec/controllers/stream_entries_controller_spec.rb +++ b/spec/controllers/stream_entries_controller_spec.rb @@ -88,14 +88,12 @@ RSpec.describe StreamEntriesController, type: :controller do describe 'GET #embed' do include_examples 'before_action', :embed - it 'returns embedded view of status' do + it 'redirects to new embed page' do status = Fabricate(:status) get :embed, params: { account_username: status.account.username, id: status.stream_entry.id } - expect(response).to have_http_status(:success) - expect(response.headers['X-Frame-Options']).to eq 'ALLOWALL' - expect(response).to render_template(layout: 'embedded') + expect(response).to redirect_to(embed_short_account_status_url(status.account, status)) end end end diff --git a/spec/lib/status_finder_spec.rb b/spec/lib/status_finder_spec.rb new file mode 100644 index 000000000..5c2f2dbe8 --- /dev/null +++ b/spec/lib/status_finder_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe StatusFinder do + include RoutingHelper + + describe '#status' do + context 'with a status url' do + let(:status) { Fabricate(:status) } + let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) } + subject { described_class.new(url) } + + it 'finds the stream entry' do + expect(subject.status).to eq(status) + end + + it 'raises an error if action is not :show' do + recognized = Rails.application.routes.recognize_path(url) + expect(recognized).to receive(:[]).with(:action).and_return(:create) + expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized) + + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with a stream entry url' do + let(:stream_entry) { Fabricate(:stream_entry) } + let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } + subject { described_class.new(url) } + + it 'finds the stream entry' do + expect(subject.status).to eq(stream_entry.status) + end + end + + context 'with a plausible url' do + let(:url) { 'https://example.com/users/test/updates/123/embed' } + subject { described_class.new(url) } + + it 'raises an error' do + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with an unrecognized url' do + let(:url) { 'https://example.com/about' } + subject { described_class.new(url) } + + it 'raises an error' do + expect { subject.status }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/lib/stream_entry_finder_spec.rb b/spec/lib/stream_entry_finder_spec.rb deleted file mode 100644 index 64e03c36a..000000000 --- a/spec/lib/stream_entry_finder_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe StreamEntryFinder do - include RoutingHelper - - describe '#stream_entry' do - context 'with a status url' do - let(:status) { Fabricate(:status) } - let(:url) { short_account_status_url(account_username: status.account.username, id: status.id) } - subject { described_class.new(url) } - - it 'finds the stream entry' do - expect(subject.stream_entry).to eq(status.stream_entry) - end - - it 'raises an error if action is not :show' do - recognized = Rails.application.routes.recognize_path(url) - expect(recognized).to receive(:[]).with(:action).and_return(:create) - expect(Rails.application.routes).to receive(:recognize_path).with(url).and_return(recognized) - - expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with a stream entry url' do - let(:stream_entry) { Fabricate(:stream_entry) } - let(:url) { account_stream_entry_url(stream_entry.account, stream_entry) } - subject { described_class.new(url) } - - it 'finds the stream entry' do - expect(subject.stream_entry).to eq(stream_entry) - end - end - - context 'with a plausible url' do - let(:url) { 'https://example.com/users/test/updates/123/embed' } - subject { described_class.new(url) } - - it 'raises an error' do - expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with an unrecognized url' do - let(:url) { 'https://example.com/about' } - subject { described_class.new(url) } - - it 'raises an error' do - expect { subject.stream_entry }.to raise_error(ActiveRecord::RecordNotFound) - end - end - end -end -- cgit From d1a78eba1558004f69ab8933b08ffe0093671546 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 31 Aug 2017 03:38:35 +0200 Subject: Embed modal (#4748) * Embed modal * Proxy OEmbed requests from web UI --- app/controllers/api/web/embeds_controller.rb | 17 +++++ .../features/status/components/action_bar.js | 8 +++ app/javascript/mastodon/features/status/index.js | 5 ++ .../mastodon/features/ui/components/embed_modal.js | 84 ++++++++++++++++++++++ .../mastodon/features/ui/components/modal_root.js | 2 + .../mastodon/features/ui/util/async-components.js | 4 ++ app/javascript/packs/public.js | 4 ++ app/javascript/styles/components.scss | 61 +++++++++++++++- app/serializers/oembed_serializer.rb | 2 +- config/routes.rb | 1 + 10 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/web/embeds_controller.rb create mode 100644 app/javascript/mastodon/features/ui/components/embed_modal.js (limited to 'app/controllers/api') diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb new file mode 100644 index 000000000..2ed516161 --- /dev/null +++ b/app/controllers/api/web/embeds_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Api::Web::EmbedsController < Api::BaseController + respond_to :json + + before_action :require_user! + + def create + status = StatusFinder.new(params[:url]).status + render json: status, serializer: OEmbedSerializer, width: 400 + rescue ActiveRecord::RecordNotFound + oembed = OEmbed::Providers.get(params[:url]) + render json: Oj.dump(oembed.fields) + rescue OEmbed::NotFound + render json: {}, status: :not_found + end +end diff --git a/app/javascript/mastodon/features/status/components/action_bar.js b/app/javascript/mastodon/features/status/components/action_bar.js index c4a614677..9431b11c1 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.js +++ b/app/javascript/mastodon/features/status/components/action_bar.js @@ -16,6 +16,7 @@ const messages = defineMessages({ share: { id: 'status.share', defaultMessage: 'Share' }, pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, + embed: { id: 'status.embed', defaultMessage: 'Embed' }, }); @injectIntl @@ -34,6 +35,7 @@ export default class ActionBar extends React.PureComponent { onMention: PropTypes.func.isRequired, onReport: PropTypes.func, onPin: PropTypes.func, + onEmbed: PropTypes.func, me: PropTypes.number.isRequired, intl: PropTypes.object.isRequired, }; @@ -73,11 +75,17 @@ export default class ActionBar extends React.PureComponent { }); } + handleEmbed = () => { + this.props.onEmbed(this.props.status); + } + render () { const { status, me, intl } = this.props; let menu = []; + menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); + if (me === status.getIn(['account', 'id'])) { if (['public', 'unlisted'].indexOf(status.get('visibility')) !== -1) { menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 84e717a12..c614f6acb 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -147,6 +147,10 @@ export default class Status extends ImmutablePureComponent { this.props.dispatch(initReport(status.get('account'), status)); } + handleEmbed = (status) => { + this.props.dispatch(openModal('EMBED', { url: status.get('url') })); + } + renderChildren (list) { return list.map(id => ); } @@ -198,6 +202,7 @@ export default class Status extends ImmutablePureComponent { onMention={this.handleMentionClick} onReport={this.handleReport} onPin={this.handlePin} + onEmbed={this.handleEmbed} /> {descendants} diff --git a/app/javascript/mastodon/features/ui/components/embed_modal.js b/app/javascript/mastodon/features/ui/components/embed_modal.js new file mode 100644 index 000000000..992aed8a3 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/embed_modal.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import axios from 'axios'; + +@injectIntl +export default class EmbedModal extends ImmutablePureComponent { + + static propTypes = { + url: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + } + + state = { + loading: false, + oembed: null, + }; + + componentDidMount () { + const { url } = this.props; + + this.setState({ loading: true }); + + axios.post('/api/web/embed', { url }).then(res => { + this.setState({ loading: false, oembed: res.data }); + + const iframeDocument = this.iframe.contentWindow.document; + + iframeDocument.open(); + iframeDocument.write(res.data.html); + iframeDocument.close(); + + iframeDocument.body.style.margin = 0; + this.iframe.height = iframeDocument.body.scrollHeight + 'px'; + }); + } + + setIframeRef = c => { + this.iframe = c; + } + + handleTextareaClick = (e) => { + e.target.select(); + } + + render () { + const { oembed } = this.state; + + return ( +
+

+ +
+

+ +

+ + + +

+ +

+ +