diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 92ccb8061..f301666db 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -7,8 +7,9 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
def index
- @statuses = load_statuses
- render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ @statuses = load_statuses
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
+ render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
private
diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb
index aa3fb88f0..f666a1d8d 100644
--- a/app/controllers/api/v1/bookmarks_controller.rb
+++ b/app/controllers/api/v1/bookmarks_controller.rb
@@ -6,8 +6,9 @@ class Api::V1::BookmarksController < Api::BaseController
after_action :insert_pagination_headers
def index
- @statuses = load_statuses
- render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ @statuses = load_statuses
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
+ render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
private
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index 21836bc17..651a057d5 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -6,8 +6,9 @@ class Api::V1::FavouritesController < Api::BaseController
after_action :insert_pagination_headers
def index
- @statuses = load_statuses
- render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ @statuses = load_statuses
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
+ render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
private
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 106fc8224..d8f4db42f 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -30,8 +30,9 @@ class Api::V1::StatusesController < Api::BaseController
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
statuses = [@status] + @context.ancestors + @context.descendants
+ accountIds = statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
- render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
+ render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
def create
@@ -46,7 +47,8 @@ class Api::V1::StatusesController < Api::BaseController
application: doorkeeper_token.application,
poll: status_params[:poll],
idempotency: request.headers['Idempotency-Key'],
- with_rate_limit: true)
+ with_rate_limit: true,
+ quote_id: status_params[:quote_id].presence)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
@@ -85,6 +87,7 @@ class Api::V1::StatusesController < Api::BaseController
:spoiler_text,
:visibility,
:scheduled_at,
+ :quote_id,
media_ids: [],
poll: [
:multiple,
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index ae6dbcb8b..c07e1a820 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -6,11 +6,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
- @statuses = load_statuses
+ @statuses = load_statuses
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
+ account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id),
status: account_home_feed.regenerating? ? 206 : 200
end
diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb
index a15eae468..bc13ed5ec 100644
--- a/app/controllers/api/v1/timelines/list_controller.rb
+++ b/app/controllers/api/v1/timelines/list_controller.rb
@@ -9,9 +9,12 @@ class Api::V1::Timelines::ListController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
+
render json: @statuses,
each_serializer: REST::StatusSerializer,
- relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
+ relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id),
+ account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
private
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index d253b744f..1a778012b 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -5,8 +5,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
- @statuses = load_statuses
- render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ @statuses = load_statuses
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
+
+ render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
private
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 64a1db58d..c7afd4580 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -5,8 +5,9 @@ class Api::V1::Timelines::TagController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
- @statuses = load_statuses
- render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ @statuses = load_statuses
+ accountIds = @statuses.filter(&:quote?).map { |status| status.quote.account_id }.uniq
+ render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), account_relationships: AccountRelationshipsPresenter.new(accountIds, current_user&.account_id)
end
private
diff --git a/app/javascript/mastodon/actions/bookmarks.js b/app/javascript/mastodon/actions/bookmarks.js
index 544ed2ff2..b3e0a2ab4 100644
--- a/app/javascript/mastodon/actions/bookmarks.js
+++ b/app/javascript/mastodon/actions/bookmarks.js
@@ -1,5 +1,7 @@
+import { fetchRelationships } from './accounts';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
+import { uniq } from '../utils/uniq';
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
@@ -20,6 +22,7 @@ export function fetchBookmarkedStatuses() {
api(getState).get('/api/v1/bookmarks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchBookmarkedStatusesFail(error));
@@ -61,6 +64,7 @@ export function expandBookmarkedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandBookmarkedStatusesFail(error));
diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js
index 6e14065d6..c0f0f3acc 100644
--- a/app/javascript/mastodon/actions/boosts.js
+++ b/app/javascript/mastodon/actions/boosts.js
@@ -11,7 +11,7 @@ export function initBoostModal(props) {
dispatch({
type: BOOSTS_INIT_MODAL,
- privacy
+ privacy,
});
dispatch(openModal('BOOST', props));
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 891403969..fb4d26cf2 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -20,6 +20,8 @@ export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL';
export const COMPOSE_REPLY = 'COMPOSE_REPLY';
export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL';
export const COMPOSE_DIRECT = 'COMPOSE_DIRECT';
+export const COMPOSE_QUOTE = 'COMPOSE_QUOTE';
+export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL';
export const COMPOSE_MENTION = 'COMPOSE_MENTION';
export const COMPOSE_RESET = 'COMPOSE_RESET';
export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST';
@@ -100,6 +102,23 @@ export function cancelReplyCompose() {
};
};
+export function quoteCompose(status, routerHistory) {
+ return (dispatch, getState) => {
+ dispatch({
+ type: COMPOSE_QUOTE,
+ status: status,
+ });
+
+ ensureComposeIsVisible(getState, routerHistory);
+ };
+};
+
+export function cancelQuoteCompose() {
+ return {
+ type: COMPOSE_QUOTE_CANCEL,
+ };
+};
+
export function resetCompose() {
return {
type: COMPOSE_RESET,
@@ -147,6 +166,7 @@ export function submitCompose(routerHistory) {
spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
visibility: getState().getIn(['compose', 'privacy']),
poll: getState().getIn(['compose', 'poll'], null),
+ quote_id: getState().getIn(['compose', 'quote_from'], null),
}, {
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js
index 9448b1efe..9b28ac4c4 100644
--- a/app/javascript/mastodon/actions/favourites.js
+++ b/app/javascript/mastodon/actions/favourites.js
@@ -1,5 +1,7 @@
+import { fetchRelationships } from './accounts';
import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer';
+import { uniq } from '../utils/uniq';
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
@@ -20,6 +22,7 @@ export function fetchFavouritedStatuses() {
api(getState).get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
@@ -64,6 +67,7 @@ export function expandFavouritedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id))));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));
diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
index f4372fb31..35032fc70 100644
--- a/app/javascript/mastodon/actions/importer/index.js
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -70,6 +70,10 @@ export function importFetchedStatuses(statuses) {
processStatus(status.reblog);
}
+ if (status.quote && status.quote.id) {
+ processStatus(status.quote);
+ }
+
if (status.poll && status.poll.id) {
pushUnique(polls, normalizePoll(status.poll));
}
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
index abd5681d4..1b9d781a7 100644
--- a/app/javascript/mastodon/actions/importer/normalizer.js
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -60,7 +60,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.search_index = normalOldStatus.get('search_index');
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
+ normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
normalStatus.hidden = normalOldStatus.get('hidden');
+ normalStatus.quote = normalOldStatus.get('quote');
+ normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
} else {
// If the status has a CW but no contents, treat the CW as if it were the
// status' contents, to avoid having a CW toggle with seemingly no effect.
@@ -77,6 +80,29 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
+
+ if (status.quote && status.quote.id) {
+ const quote_spoilerText = status.quote.spoiler_text || '';
+ const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n'); + + const quote_emojiMap = makeEmojiMap(normalStatus.quote); + + const quote_account_emojiMap = makeEmojiMap(status.quote.account); + const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name; + normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap); + normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent; + let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement; + Array.from(docElem.querySelectorAll('p,br'), line => { + let parentNode = line.parentNode; + if (line.nextSibling) { + parentNode.insertBefore(document.createTextNode(' '), line.nextSibling); + } + }); + let _contentHtml = docElem.textContent; + normalStatus.quote.contentHtml = '
'+emojify(_contentHtml.substr(0, 150), quote_emojiMap) + (_contentHtml.substr(150) ? '...' : '')+'
'; + normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap); + normalStatus.quote_hidden = expandSpoilers ? false : quote_spoilerText.length > 0 || normalStatus.quote.sensitive; + } } return normalStatus; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 3464ac995..22f1e0cbb 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -46,7 +46,7 @@ defineMessages({ }); const fetchRelatedRelationships = (dispatch, notifications) => { - const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); + const accountIds = notifications.map(item => item.account.id); if (accountIds.length > 0) { dispatch(fetchRelationships(accountIds)); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 3fc7c0702..0c520a796 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -30,6 +30,9 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE'; export const REDRAFT = 'REDRAFT'; +export const QUOTE_REVEAL = 'QUOTE_REVEAL'; +export const QUOTE_HIDE = 'QUOTE_HIDE'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -272,3 +275,25 @@ export function toggleStatusCollapse(id, isCollapsed) { isCollapsed, }; } + +export function hideQuote(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: QUOTE_HIDE, + ids, + }; +}; + +export function revealQuote(ids) { + if (!Array.isArray(ids)) { + ids = [ids]; + } + + return { + type: QUOTE_REVEAL, + ids, + }; +}; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 31ae09e4a..d82bf1d0d 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,9 +1,11 @@ +import { fetchRelationships } from './accounts'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; import api, { getLinks } from 'mastodon/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'mastodon/compare_id'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; +import { uniq } from '../utils/uniq'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -39,6 +41,7 @@ export function updateTimeline(timeline, status, accept) { } dispatch(importFetchedStatus(status)); + dispatch(fetchRelationships([status.reblog ? status.reblog.account.id : status.account.id, status.quote ? status.quote.account.id : null].filter(function(e){return e}))); dispatch({ type: TIMELINE_UPDATE, @@ -111,6 +114,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); + dispatch(fetchRelationships(uniq(response.data.map(item => item.reblog ? item.reblog.account.id : item.account.id).concat(response.data.map(item => item.quote ? item.quote.account.id : null)).filter(function(e){return e})))); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); if (timelineId === 'home') { diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 2e7ce2e60..5c303a1a0 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -236,10 +236,12 @@ class MediaGallery extends React.PureComponent { visible: PropTypes.bool, autoplay: PropTypes.bool, onToggleVisibility: PropTypes.func, + quote: PropTypes.bool, }; static defaultProps = { standalone: false, + quote: false, }; state = { @@ -310,7 +312,7 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props; + const { media, intl, sensitive, height, defaultWidth, standalone, autoplay, quote } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; @@ -332,6 +334,10 @@ class MediaGallery extends React.PureComponent { const size = media.take(4).size; const uncached = media.every(attachment => attachment.get('type') === 'unknown'); + if (quote && style.height) { + style.height /= 2; + } + if (standalone && this.isFullSizeEligible()) { children =(.+)<\/p>$/, '\1')
+ html = Sanitize.clean(html).delete("\n").truncate(150)
+ html = encode_custom_emojis(html, status.emojis) if options[:custom_emojify]
+ html.html_safe
+ end
+
def reformat(html)
sanitize(html, Sanitize::Config::MASTODON_STRICT)
rescue ArgumentError
@@ -206,6 +218,12 @@ class Formatter
end
# rubocop:enable Metrics/BlockNesting
+ def quotify(html, status)
+ url = ActivityPub::TagManager.instance.url_for(status.quote)
+ link = encode_and_link_urls(url)
+ html.sub(/(<[^>]+>)\z/, "
QT: #{link}\\1")
+ end
+
def rewrite(text, entities)
text = text.to_s
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index 2d5ebfca3..1af53ed23 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -21,7 +21,7 @@ module AccountAvatar
has_attached_file :avatar, styles: ->(f) { avatar_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
validates_attachment_content_type :avatar, content_type: IMAGE_MIME_TYPES
validates_attachment_size :avatar, less_than: LIMIT
- remotable_attachment :avatar, LIMIT
+ remotable_attachment :avatar, LIMIT, suppress_errors: false
end
def avatar_original_url
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index 067e166eb..72a3d0566 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -22,7 +22,7 @@ module AccountHeader
has_attached_file :header, styles: ->(f) { header_styles(f) }, convert_options: { all: '-strip' }, processors: [:lazy_thumbnail]
validates_attachment_content_type :header, content_type: IMAGE_MIME_TYPES
validates_attachment_size :header, less_than: LIMIT
- remotable_attachment :header, LIMIT
+ remotable_attachment :header, LIMIT, suppress_errors: false
end
def header_original_url
diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb
index 56b9c0164..ffe8a7565 100644
--- a/app/models/concerns/remotable.rb
+++ b/app/models/concerns/remotable.rb
@@ -28,9 +28,11 @@ module Remotable
end
rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError => e
Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
+ public_send("#{attachment_name}=", nil) if public_send("#{attachment_name}_file_name").present?
raise e unless suppress_errors
rescue Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Paperclip::Error, Mastodon::DimensionsValidationError, Mastodon::StreamValidationError => e
Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}"
+ public_send("#{attachment_name}=", nil) if public_send("#{attachment_name}_file_name").present?
end
nil
diff --git a/app/models/status.rb b/app/models/status.rb
index 847921ac2..6e088570d 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -23,6 +23,7 @@
# in_reply_to_account_id :bigint(8)
# poll_id :bigint(8)
# deleted_at :datetime
+# quote_id :bigint(8)
#
class Status < ApplicationRecord
@@ -55,6 +56,7 @@ class Status < ApplicationRecord
belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true
+ belongs_to :quote, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quoted, optional: true
has_many :favourites, inverse_of: :status, dependent: :destroy
has_many :bookmarks, inverse_of: :status, dependent: :destroy
@@ -63,6 +65,7 @@ class Status < ApplicationRecord
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify
+ has_many :quoted, foreign_key: 'quote_id', class_name: 'Status', inverse_of: :quote, dependent: :nullify
has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards
@@ -77,6 +80,7 @@ class Status < ApplicationRecord
validates_with DisallowedHashtagsValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
+ validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote?
accepts_nested_attributes_for :poll
@@ -169,6 +173,14 @@ class Status < ApplicationRecord
!reblog_of_id.nil?
end
+ def quote?
+ !quote_id.nil? && quote
+ end
+
+ def quote_visibility
+ quote&.visibility
+ end
+
def within_realtime_window?
created_at >= REAL_TIME_WINDOW.ago
end
@@ -229,7 +241,7 @@ class Status < ApplicationRecord
fields = [spoiler_text, text]
fields += preloadable_poll.options unless preloadable_poll.nil?
- @emojis = CustomEmoji.from_text(fields.join(' '), account.domain)
+ @emojis = CustomEmoji.from_text(fields.join(' '), account.domain) + (quote? ? CustomEmoji.from_text([quote.spoiler_text, quote.text].join(' '), quote.account.domain) : [])
end
def replies_count
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index d92aae7b3..a7d948976 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -177,7 +177,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
end
def href
- explore_hashtag_url(object)
+ tag_url(object)
end
def name
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index 6f9e1ca63..43e6e626c 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -9,6 +9,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:atom_uri, :in_reply_to_atom_uri,
:conversation
+ attribute :quote_url, if: -> { object.quote? }
+ attribute :misskey_quote, key: :_misskey_quote, if: -> { object.quote? }
+ attribute :misskey_content, key: :_misskey_content, if: -> { object.quote? }
attribute :content
attribute :content_map, if: :language?
@@ -125,6 +128,16 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
end
end
+ def quote_url
+ ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote?
+ end
+
+ alias misskey_quote quote_url
+
+ def misskey_content
+ object.text if object.quote?
+ end
+
def local?
object.account.local?
end
diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb
index d39092b56..6683ac48f 100644
--- a/app/serializers/rest/instance_serializer.rb
+++ b/app/serializers/rest/instance_serializer.rb
@@ -5,7 +5,8 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail,
- :languages, :registrations, :approval_required, :invites_enabled
+ :languages, :registrations, :approval_required, :invites_enabled,
+ :feature_quote
has_one :contact_account, serializer: REST::AccountSerializer
@@ -69,6 +70,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
Setting.min_invite_role == 'user'
end
+ def feature_quote
+ true
+ end
+
private
def instance_presenter
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index bb6df90b7..f49b365bf 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -15,6 +15,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
attribute :content, unless: :source_requested?
attribute :text, if: :source_requested?
+ attribute :quote_id, if: -> { object.quote? }
+
belongs_to :reblog, serializer: REST::StatusSerializer
belongs_to :application, if: :show_application?
belongs_to :account, serializer: REST::AccountSerializer
@@ -39,6 +41,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.in_reply_to_account_id&.to_s
end
+ def quote_id
+ object.quote_id.to_s
+ end
+
def current_user?
!current_user.nil?
end
@@ -167,3 +173,23 @@ class REST::StatusSerializer < ActiveModel::Serializer
end
end
end
+
+class REST::NestedQuoteSerializer < REST::StatusSerializer
+ attribute :quote do
+ nil
+ end
+ attribute :quote_muted, if: :current_user?
+
+ def quote_muted
+ if instance_options && instance_options[:account_relationships]
+ instance_options[:account_relationships].muting[object.account_id] ? true : false || instance_options[:account_relationships].blocking[object.account_id] || instance_options[:account_relationships].blocked_by[object.account_id] || instance_options[:account_relationships].domain_blocking[object.account_id] || false
+ else
+ current_user.account.muting?(object.account) || object.account.blocking?(current_user.account) || current_user.account.blocking?(object.account) || current_user.account.domain_blocking?(object.account.domain)
+ end
+ end
+
+end
+
+class REST::StatusSerializer < ActiveModel::Serializer
+ belongs_to :quote, serializer: REST::NestedQuoteSerializer
+end
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index bb2e8f665..7e268f4d4 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -106,8 +106,16 @@ class ActivityPub::ProcessAccountService < BaseService
end
def set_fetchable_attributes!
- @account.avatar_remote_url = image_url('icon') || '' unless skip_download?
- @account.header_remote_url = image_url('image') || '' unless skip_download?
+ begin
+ @account.avatar_remote_url = image_url('icon') || '' unless skip_download?
+ rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
+ RedownloadAvatarWorker.perform_in(rand(30..600).seconds, @account.id)
+ end
+ begin
+ @account.header_remote_url = image_url('image') || '' unless skip_download?
+ rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
+ RedownloadHeaderWorker.perform_in(rand(30..600).seconds, @account.id)
+ end
@account.statuses_count = outbox_total_items if outbox_total_items.present?
@account.following_count = following_total_items if following_total_items.present?
@account.followers_count = followers_total_items if followers_total_items.present?
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index fa1636e41..2defecd74 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -67,7 +67,7 @@ class FetchLinkCardService < BaseService
urls = @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
else
html = Nokogiri::HTML(@status.text)
- links = html.css('a')
+ links = html.css(':not(.quote-inline) > a')
urls = links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
end
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 0a383d6a3..f594b02a2 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -26,11 +26,13 @@ class PostStatusService < BaseService
@options = options
@text = @options[:text] || ''
@in_reply_to = @options[:thread]
+ @quote_id = @options[:quote_id]
return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
validate_media!
preprocess_attributes!
+ preprocess_quote!
if scheduled?
schedule_status!
@@ -47,6 +49,19 @@ class PostStatusService < BaseService
private
+ def status_from_uri(uri)
+ ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
+ end
+
+ def quote_from_url(url)
+ return nil if url.nil?
+
+ quote = ResolveURLService.new.call(url)
+ status_from_uri(quote.uri) if quote
+ rescue
+ nil
+ end
+
def preprocess_attributes!
@sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@@ -54,10 +69,21 @@ class PostStatusService < BaseService
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?
+ if @quote_id.nil? && md = @text.match(/QT:\s*\[\s*(https:\/\/.+?)\s*\]/)
+ @quote_id = quote_from_url(md[1])&.id
+ @text.sub!(/QT:\s*\[.*?\]/, '')
+ end
rescue ArgumentError
raise ActiveRecord::RecordInvalid
end
+ def preprocess_quote!
+ if @quote_id.present?
+ quote = Status.find(@quote_id)
+ @quote_id = quote.reblog_of_id.to_s if quote.reblog?
+ end
+ end
+
def process_status!
# The following transaction block is needed to wrap the UPDATEs to
# the media attachments when the status is created
@@ -164,6 +190,7 @@ class PostStatusService < BaseService
language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account),
application: @options[:application],
rate_limit: @options[:with_rate_limit],
+ quote_id: @quote_id,
}.compact
end
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index daf164949..25231d440 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -25,6 +25,9 @@
- if status.preloadable_poll
= render_poll_component(status)
+ - if status.quote?
+ = render partial: "quote_status", locals: {status: status.quote}
+
- if !status.media_attachments.empty?
- if status.media_attachments.first.video?
= render_video_component(status, width: 670, height: 380, detailed: true)
diff --git a/app/views/statuses/_quote_status.html.haml b/app/views/statuses/_quote_status.html.haml
new file mode 100644
index 000000000..fa0b7b1b1
--- /dev/null
+++ b/app/views/statuses/_quote_status.html.haml
@@ -0,0 +1,35 @@
+.status.quote-status{ dataurl: ActivityPub::TagManager.instance.url_for(status) }
+ = link_to ActivityPub::TagManager.instance.url_for(status.account), class: 'status__display-name u-url', target: stream_link_target, rel: 'noopener' do
+ .status__avatar
+ %div
+ = image_tag status.account.avatar_static_url, width: 18, height: 18, alt: '', class: 'u-photo account__avatar'
+ %span.display-name
+ %bdi
+ %strong.display-name__html.p-name.emojify= display_name(status.account, custom_emojify: true)
+
+ %span.display-name__account
+ = acct(status.account)
+ = fa_icon('lock') if status.account.locked?
+
+ .status__content.emojify<
+ - if status.spoiler_text?
+ %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
+ %span.p-summary> #{Formatter.instance.format_spoiler(status)}
+ %button.status__content__spoiler-link= t('statuses.show_more')
+ .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}" }
+ = Formatter.instance.format_in_quote(status, custom_emojify: true)
+
+ - if !status.media_attachments.empty?
+ - if status.media_attachments.first.video?
+ - video = status.media_attachments.first
+ = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), frameRate: video.file.meta.dig('original', 'frame_rate'), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description, media: [ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer)].as_json, quote: true do
+ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ - elsif status.media_attachments.first.audio?
+ - audio = status.media_attachments.first
+ = react_component :audio, src: audio.file.url(:original), height: 60, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do
+ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ - else
+ = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, quote: true do
+ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ - elsif status.preview_card
+ = react_component :card, maxDescription: 10, card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, quote: true
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index 728e6b9b0..d134b1008 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -34,6 +34,9 @@
- if status.preloadable_poll
= render_poll_component(status)
+ - if status.quote?
+ = render partial: "statuses/quote_status", locals: {status: status.quote}
+
- if !status.media_attachments.empty?
- if status.media_attachments.first.video?
= render_video_component(status, width: 610, height: 343)
diff --git a/app/workers/redownload_avatar_worker.rb b/app/workers/redownload_avatar_worker.rb
new file mode 100644
index 000000000..df17b7718
--- /dev/null
+++ b/app/workers/redownload_avatar_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class RedownloadAvatarWorker
+ include Sidekiq::Worker
+ include ExponentialBackoff
+ include JsonLdHelper
+
+ sidekiq_options queue: 'pull', retry: 7
+
+ def perform(id)
+ account = Account.find(id)
+
+ return if account.suspended? || DomainBlock.rule_for(account.domain)&.reject_media?
+ return if account.avatar_remote_url.blank? || account.avatar_file_name.present?
+
+ account.reset_avatar!
+ account.save!
+ rescue ActiveRecord::RecordNotFound
+ # Do nothing
+ rescue Mastodon::UnexpectedResponseError => e
+ response = e.response
+
+ if response_error_unsalvageable?(response)
+ # Give up
+ else
+ raise e
+ end
+ end
+end
diff --git a/app/workers/redownload_header_worker.rb b/app/workers/redownload_header_worker.rb
new file mode 100644
index 000000000..3b142ec5f
--- /dev/null
+++ b/app/workers/redownload_header_worker.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class RedownloadHeaderWorker
+ include Sidekiq::Worker
+ include ExponentialBackoff
+ include JsonLdHelper
+
+ sidekiq_options queue: 'pull', retry: 7
+
+ def perform(id)
+ account = Account.find(id)
+
+ return if account.suspended? || DomainBlock.rule_for(account.domain)&.reject_media?
+ return if account.header_remote_url.blank? || account.header_file_name.present?
+
+ account.reset_header!
+ account.save!
+ rescue ActiveRecord::RecordNotFound
+ # Do nothing
+ rescue Mastodon::UnexpectedResponseError => e
+ response = e.response
+
+ if response_error_unsalvageable?(response)
+ # Give up
+ else
+ raise e
+ end
+ end
+end
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
index 2d47a9aaf..35f2c3178 100644
--- a/config/brakeman.ignore
+++ b/config/brakeman.ignore
@@ -7,7 +7,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/report.rb",
- "line": 112,
+ "line": 113,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "Admin::ActionLog.from(\"(#{[Admin::ActionLog.where(:target_type => \"Report\", :target_id => id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Account\", :target_id => target_account_id, :created_at => ((created_at..updated_at))).unscope(:order), Admin::ActionLog.where(:target_type => \"Status\", :target_id => status_ids, :created_at => ((created_at..updated_at))).unscope(:order)].map do\n \"(#{query.to_sql})\"\n end.join(\" UNION ALL \")}) AS admin_action_logs\")",
"render_path": null,
@@ -67,7 +67,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
- "line": 491,
+ "line": 479,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "find_by_sql([\" WITH first_degree AS (\\n SELECT target_account_id\\n FROM follows\\n WHERE account_id = ?\\n UNION ALL\\n SELECT ?\\n )\\n SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)\\n WHERE accounts.id IN (SELECT * FROM first_degree)\\n AND #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, account.id, limit, offset])",
"render_path": null,
@@ -120,6 +120,26 @@
"confidence": "High",
"note": ""
},
+ {
+ "warning_type": "Mass Assignment",
+ "warning_code": 105,
+ "fingerprint": "874be88fedf4c680926845e9a588d3197765a6ccbfdd76466b44cc00151c612e",
+ "check_name": "PermitAttributes",
+ "message": "Potentially dangerous key allowed for mass assignment",
+ "file": "app/controllers/api/v1/admin/reports_controller.rb",
+ "line": 78,
+ "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
+ "code": "params.permit(:resolved, :account_id, :target_account_id)",
+ "render_path": null,
+ "location": {
+ "type": "method",
+ "class": "Api::V1::Admin::ReportsController",
+ "method": "filter_params"
+ },
+ "user_input": ":account_id",
+ "confidence": "High",
+ "note": ""
+ },
{
"warning_type": "SQL Injection",
"warning_code": 0,
@@ -127,7 +147,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
- "line": 460,
+ "line": 448,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, limit, offset])",
"render_path": null,
@@ -207,7 +227,7 @@
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
- "line": 507,
+ "line": 495,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, limit, offset])",
"render_path": null,
@@ -241,6 +261,6 @@
"note": ""
}
],
- "updated": "2020-12-07 01:17:13 +0100",
- "brakeman_version": "4.10.0"
+ "updated": "2021-05-11 20:22:27 +0900",
+ "brakeman_version": "5.0.1"
}
diff --git a/db/migrate/20180419235016_add_quote_id_to_statuses.rb b/db/migrate/20180419235016_add_quote_id_to_statuses.rb
new file mode 100644
index 000000000..d7550b248
--- /dev/null
+++ b/db/migrate/20180419235016_add_quote_id_to_statuses.rb
@@ -0,0 +1,5 @@
+class AddQuoteIdToStatuses < ActiveRecord::Migration[5.1]
+ def change
+ add_column :statuses, :quote_id, :bigint, null: true, default: nil
+ end
+end
diff --git a/db/migrate/20200301102028_add_index_to_statuses_quote_id.rb b/db/migrate/20200301102028_add_index_to_statuses_quote_id.rb
new file mode 100644
index 000000000..45293db98
--- /dev/null
+++ b/db/migrate/20200301102028_add_index_to_statuses_quote_id.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIndexToStatusesQuoteId < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def change
+ add_index :statuses, :quote_id, algorithm: :concurrently
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 583bdf317..fff9e49ef 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -815,11 +815,13 @@ ActiveRecord::Schema.define(version: 2021_05_07_001928) do
t.bigint "in_reply_to_account_id"
t.bigint "poll_id"
t.datetime "deleted_at"
+ t.bigint "quote_id"
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["id", "account_id"], name: "index_statuses_public_20200119", order: { id: :desc }, where: "((deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
+ t.index ["quote_id"], name: "index_statuses_on_quote_id"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
t.index ["uri"], name: "index_statuses_on_uri", unique: true
end
diff --git a/lib/paperclip/gif_transcoder.rb b/lib/paperclip/gif_transcoder.rb
index 74aa1a0b2..d14465c01 100644
--- a/lib/paperclip/gif_transcoder.rb
+++ b/lib/paperclip/gif_transcoder.rb
@@ -108,9 +108,11 @@ module Paperclip
final_file = Paperclip::Transcoder.make(file, options, attachment)
- attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.mp4'
- attachment.instance.file_content_type = 'video/mp4'
- attachment.instance.type = MediaAttachment.types[:gifv]
+ if options[:style] == :original
+ attachment.instance.file_file_name = File.basename(attachment.instance.file_file_name, '.*') + '.mp4'
+ attachment.instance.file_content_type = 'video/mp4'
+ attachment.instance.type = MediaAttachment.types[:gifv]
+ end
final_file
end
@@ -118,7 +120,7 @@ module Paperclip
private
def needs_convert?
- options[:style] == :original && GifReader.animated?(file.path)
+ GifReader.animated?(file.path)
end
end
end
diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb
index a2e1d9d01..de6b132ee 100644
--- a/lib/sanitize_ext/sanitize_config.rb
+++ b/lib/sanitize_ext/sanitize_config.rb
@@ -31,6 +31,7 @@ class Sanitize
next true if /^(h|p|u|dt|e)-/.match?(e) # microformats classes
next true if /^(mention|hashtag)$/.match?(e) # semantic classes
next true if /^(ellipsis|invisible)$/.match?(e) # link formatting classes
+ next true if /^quote-inline$/.match?(e) # quote inline classes
end
node['class'] = class_list.join(' ')