From 78e869338886e004589bba4ca7f2da978386f035 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:00:29 +0000 Subject: [PATCH 01/37] services: link fetcher: do not fetch links for quotes --- app/services/fetch_link_card_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index e5b5b730ec..e5c0448925 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -74,7 +74,7 @@ class FetchLinkCardService < BaseService @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize } else document = Nokogiri::HTML(@status.text) - links = document.css('a') + links = document.css(':not(.quote-inline) > a') links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize) end -- 2.41.0 From b1bce9d1939ab5b7e690eb08e8b81faca1d95f6b Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:07:07 +0000 Subject: [PATCH 02/37] models: status: add support for quoting --- app/models/status.rb | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/models/status.rb b/app/models/status.rb index 044816be76..e84bff3d60 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -28,6 +28,7 @@ # edited_at :datetime # trendable :boolean # ordered_media_attachment_ids :bigint(8) is an Array +# quote_id :bigint(8) # class Status < ApplicationRecord @@ -61,6 +62,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: :quote, optional: true has_many :favourites, inverse_of: :status, dependent: :destroy has_many :bookmarks, inverse_of: :status, dependent: :destroy @@ -70,6 +72,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 @@ -134,6 +137,17 @@ class Status < ApplicationRecord account: [:account_stat, :user], active_mentions: { account: :account_stat }, ], + quote: [ + :application, + :tags, + :preview_cards, + :media_attachments, + :conversation, + :status_stat, + :preloadable_poll, + account: [:account_stat, :user], + active_mentions: { account: :account_stat }, + ], thread: { account: :account_stat } delegate :domain, to: :account, prefix: true @@ -195,6 +209,10 @@ class Status < ApplicationRecord !reblog_of_id.nil? end + def quote? + !quote_id.nil? && quote + end + def within_realtime_window? created_at >= REAL_TIME_WINDOW.ago end @@ -259,7 +277,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 ordered_media_attachments -- 2.41.0 From 0990d5ac75cc83c841b74d58cec158b16836ea67 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:11:04 +0000 Subject: [PATCH 03/37] activitypub: note serializer: begrudgingly serialize quotes using misskey quoteUrl --- app/serializers/activitypub/note_serializer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index ca067ed9b5..6a8e13d6c2 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -9,7 +9,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, :atom_uri, :in_reply_to_atom_uri, - :conversation + :conversation, :quote_url attribute :content attribute :content_map, if: :language? @@ -149,6 +149,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end end + def quote_url + object.quote? ? ActivityPub::TagManager.instance.uri_for(object.quote) : nil + end + def local? object.account.local? end -- 2.41.0 From 6b07407820ef2e9152e68b392d136cd4a3ca6a4c Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:13:30 +0000 Subject: [PATCH 04/37] context helper: add quoteUrl as as:quoteUrl, even though its wrong --- app/helpers/context_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 2f5fecaae8..f4cf1f3a92 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -24,6 +24,7 @@ module ContextHelper voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, + quoteUrl: { 'quoteUrl' => 'as:quoteUrl' }, }.freeze def full_context -- 2.41.0 From 968bd6f0ee9c8ae4dbb84a2e1bad6250bf394068 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:16:28 +0000 Subject: [PATCH 05/37] activitypub: resolve quoted objects when new create activities are received --- app/lib/activitypub/activity/create.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index ebae129732..0dc315c53b 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -126,6 +126,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity conversation: conversation_from_uri(@object['conversation']), media_attachment_ids: process_attachments.take(4).map(&:id), poll: process_poll, + quote: quote_from_url(@object['quoteUrl']), } end end @@ -426,4 +427,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity poll.reload retry end + + def quote_from_url(url) + return nil if url.nil? + + quote = ResolveURLService.new.call(url) + status_from_uri(quote.uri) if quote + end end -- 2.41.0 From ba965bec3deae8f2dbde50ebe5cfb71bafd7b83e Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:18:17 +0000 Subject: [PATCH 06/37] statuses controller: accept quote_id parameter --- app/controllers/api/v1/statuses_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index e2e48f6337..96c38ff555 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -65,7 +65,8 @@ class Api::V1::StatusesController < Api::BaseController poll: status_params[:poll], content_type: status_params[:content_type], 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 @@ -129,6 +130,7 @@ class Api::V1::StatusesController < Api::BaseController :visibility, :language, :scheduled_at, + :quote_id, :content_type, media_ids: [], poll: [ -- 2.41.0 From ee98c0a6f8fb75f0a3970bd812b7382a470684a3 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:20:15 +0000 Subject: [PATCH 07/37] services: post status service: add quote_id to status parameters --- app/services/post_status_service.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 36592a531f..2e52aec668 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -21,6 +21,7 @@ class PostStatusService < BaseService # @option [Doorkeeper::Application] :application # @option [String] :idempotency Optional idempotency key # @option [Boolean] :with_rate_limit + # @option [String] :quote_id # @return [Status] def call(account, options = {}) @account = account @@ -179,6 +180,7 @@ class PostStatusService < BaseService application: @options[:application], content_type: @options[:content_type] || @account.user&.setting_default_content_type, rate_limit: @options[:with_rate_limit], + quote_id: @options[:quote_id], }.compact end -- 2.41.0 From 56d4b04358449a1974e33e0297e240e1e7e62ba1 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:24:07 +0000 Subject: [PATCH 08/37] views: add quote status html view --- app/views/statuses/_detailed_status.html.haml | 3 ++ app/views/statuses/_quote_status.html.haml | 35 +++++++++++++++++++ app/views/statuses/_simple_status.html.haml | 4 +++ 3 files changed, 42 insertions(+) create mode 100644 app/views/statuses/_quote_status.html.haml diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 619406d897..cbf29224d0 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -15,6 +15,9 @@ = account_action_button(status.account) + - if status.quote? + = render partial: "statuses/quote_status", locals: {status: status.quote} + .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - if status.spoiler_text? %p< diff --git a/app/views/statuses/_quote_status.html.haml b/app/views/statuses/_quote_status.html.haml new file mode 100644 index 0000000000..6fdaa9a84f --- /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), 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, 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 1e37b6cf3a..44d9673a0e 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -27,6 +27,10 @@ %span.display-name__account = acct(status.account) = fa_icon('lock') if status.account.locked? + + - if status.quote? + = render partial: "statuses/quote_status", locals: {status: status.quote} + .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - if status.spoiler_text? %p< -- 2.41.0 From 28fb5c8c52ea3cead25fd861b5b370ff51226e78 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:25:40 +0000 Subject: [PATCH 09/37] rest: status serializer: include quote data --- app/serializers/rest/status_serializer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 659c45b835..b6b09a5b37 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -23,6 +23,7 @@ class REST::StatusSerializer < ActiveModel::Serializer belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application, if: :show_application? belongs_to :account, serializer: REST::AccountSerializer + belongs_to :quote, serializer: REST::StatusSerializer has_many :ordered_media_attachments, key: :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :ordered_mentions, key: :mentions -- 2.41.0 From 1df2577b893727601c21fedbb48ddbaa5b54ce4c Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:48:37 +0000 Subject: [PATCH 10/37] db: add quote_id to statuses table --- db/schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/schema.rb b/db/schema.rb index 01a95c0f29..8bacd84d9c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -943,6 +943,7 @@ ActiveRecord::Schema.define(version: 2022_11_04_133904) do t.datetime "edited_at" t.boolean "trendable" t.bigint "ordered_media_attachment_ids", array: true + 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 ["account_id"], name: "index_statuses_on_account_id" t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)" -- 2.41.0 From 0b48ae2c3c9923b735cbf33d92ae0f03d95fd4a2 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:48:52 +0000 Subject: [PATCH 11/37] sanitizer config: add quote-inline span to allowlist --- lib/sanitize_ext/sanitize_config.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/sanitize_ext/sanitize_config.rb b/lib/sanitize_ext/sanitize_config.rb index 946543868a..be47818709 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(' ') -- 2.41.0 From a697e1da13bea4f4ff64879065b2a2eb903f2071 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 02:50:28 +0000 Subject: [PATCH 12/37] db: add quote_id migration --- db/migrate/20221224204906_add_quote_id_to_statuses.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20221224204906_add_quote_id_to_statuses.rb diff --git a/db/migrate/20221224204906_add_quote_id_to_statuses.rb b/db/migrate/20221224204906_add_quote_id_to_statuses.rb new file mode 100644 index 0000000000..b01f6520a2 --- /dev/null +++ b/db/migrate/20221224204906_add_quote_id_to_statuses.rb @@ -0,0 +1,5 @@ +class AddQuoteIdToStatuses < ActiveRecord::Migration[6.1] + def change + add_column :statuses, :quote_id, :bigint, null: true, default: nil + end +end -- 2.41.0 From 1cef1eb847b4e26ab5a96f908aea72de8a80d9cd Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 03:56:19 +0000 Subject: [PATCH 13/37] status: disallow quoting of non-public posts --- app/models/status.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/status.rb b/app/models/status.rb index e84bff3d60..c08f0c55cd 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -89,6 +89,7 @@ class Status < ApplicationRecord validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true + validates :quote_visibility, inclusion: { in: %w(public unlisted) }, if: :quote? accepts_nested_attributes_for :poll @@ -213,6 +214,10 @@ class Status < ApplicationRecord !quote_id.nil? && quote end + def quote_visibility + quote&.visibility + end + def within_realtime_window? created_at >= REAL_TIME_WINDOW.ago end -- 2.41.0 From 36955a7a56bf36de94294b46960fd280442f0529 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 04:02:08 +0000 Subject: [PATCH 14/37] status: prevent recursion when serializing --- app/serializers/rest/status_serializer.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index b6b09a5b37..f33f499a5d 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -23,7 +23,6 @@ class REST::StatusSerializer < ActiveModel::Serializer belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application, if: :show_application? belongs_to :account, serializer: REST::AccountSerializer - belongs_to :quote, serializer: REST::StatusSerializer has_many :ordered_media_attachments, key: :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :ordered_mentions, key: :mentions @@ -185,3 +184,13 @@ class REST::StatusSerializer < ActiveModel::Serializer end end end + +class REST::QuoteStatusSerializer < REST::StatusSerializer + attribute :quote do + nil + end +end + +class REST::StatusSerializer < ActiveModel::Serializer + belongs_to :quote, serializer: REST::QuoteStatusSerializer +end -- 2.41.0 From 8d86c77a58c3eb20e0db15a7aa4b498ae4af1566 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 04:04:29 +0000 Subject: [PATCH 15/37] db: add quote_id index --- .../20221224220348_add_index_to_statuses_quote_id.rb | 7 +++++++ db/schema.rb | 1 + 2 files changed, 8 insertions(+) create mode 100644 db/migrate/20221224220348_add_index_to_statuses_quote_id.rb diff --git a/db/migrate/20221224220348_add_index_to_statuses_quote_id.rb b/db/migrate/20221224220348_add_index_to_statuses_quote_id.rb new file mode 100644 index 0000000000..2a51daf883 --- /dev/null +++ b/db/migrate/20221224220348_add_index_to_statuses_quote_id.rb @@ -0,0 +1,7 @@ +class AddIndexToStatusesQuoteId < ActiveRecord::Migration[6.1] + 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 8bacd84d9c..c2d2bee569 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -951,6 +951,7 @@ ActiveRecord::Schema.define(version: 2022_11_04_133904) do 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", where: "(in_reply_to_account_id IS NOT NULL)" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", where: "(in_reply_to_id IS NOT NULL)" + 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, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)" end -- 2.41.0 From 61565488a6e6c4cadc5072b39a18dbde935d214a Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 04:51:21 +0000 Subject: [PATCH 16/37] status: support either _misskey_quote or quoteUrl for fetching quotes --- app/lib/activitypub/activity/create.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 0dc315c53b..47ab1b6963 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -126,7 +126,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity conversation: conversation_from_uri(@object['conversation']), media_attachment_ids: process_attachments.take(4).map(&:id), poll: process_poll, - quote: quote_from_url(@object['quoteUrl']), + quote: process_quote, } end end @@ -428,10 +428,23 @@ class ActivityPub::Activity::Create < ActivityPub::Activity retry end - def quote_from_url(url) + def guess_quote_url + if @object["quoteUrl"] && !@object["quoteUrl"].empty? + @object["quoteUrl"] + elsif @object["_misskey_quote"] && !@object["_misskey_quote"].empty? + @object["_misskey_quote"] + else + nil + end + end + + def process_quote + url = guess_quote_url return nil if url.nil? quote = ResolveURLService.new.call(url) status_from_uri(quote.uri) if quote + rescue + nil end end -- 2.41.0 From 14d001574c7b823929ee292851aa101502508a77 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 04:55:02 +0000 Subject: [PATCH 17/37] activitypub: case transform: support _misskey keys without messing them up --- app/lib/activitypub/case_transform.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lib/activitypub/case_transform.rb b/app/lib/activitypub/case_transform.rb index 7f716f8624..ef0c6e1f91 100644 --- a/app/lib/activitypub/case_transform.rb +++ b/app/lib/activitypub/case_transform.rb @@ -14,6 +14,8 @@ module ActivityPub::CaseTransform when String camel_lower_cache[value] ||= if value.start_with?('_:') '_:' + value.gsub(/\A_:/, '').underscore.camelize(:lower) + elsif value.start_with?('_') + value else value.underscore.camelize(:lower) end -- 2.41.0 From b36e884cc1507201c243c069813303f28dc8b881 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 04:56:56 +0000 Subject: [PATCH 18/37] activitypub: note serializer: support _misskey keys --- app/serializers/activitypub/note_serializer.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 6a8e13d6c2..c9101b4469 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -9,7 +9,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, :atom_uri, :in_reply_to_atom_uri, - :conversation, :quote_url + :conversation + + attribute :quote_url, if: -> { object.quote? } + attribute :misskey_quote, key: :_misskey_quote, if: -> { object.quote? } attribute :content attribute :content_map, if: :language? @@ -150,9 +153,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end def quote_url - object.quote? ? ActivityPub::TagManager.instance.uri_for(object.quote) : nil + ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote? end + alias misskey_quote quote_url + def local? object.account.local? end -- 2.41.0 From 0d3df3e8cf2514880f7c1896a35a3de416d78004 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 09:33:33 +0000 Subject: [PATCH 19/37] javascript: glitch: pre-process misskey quotes to remove the URL part --- .../glitch/actions/importer/normalizer.js | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 1c9f524e43..567db7eeb4 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -74,6 +74,8 @@ export function normalizeStatus(status, normalOldStatus, settings) { normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); normalStatus.hidden = normalOldStatus.get('hidden'); + normalStatus.quote = normalOldStatus.get('quote'); + normalStatus.quote_hidden = normalOldStatus.get('quote_hidden'); } else { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); @@ -83,6 +85,35 @@ export function normalizeStatus(status, normalOldStatus, settings) { normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText); + + 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('span.quote-inline'), span => span.remove()); + 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 = (quote_spoilerText.length > 0 || normalStatus.quote.sensitive) && autoHideCW(settings, quote_spoilerText); + + // delete the quote link!!!! + let parentDocElem = domParser.parseFromString(normalStatus.contentHtml, 'text/html').documentElement; + Array.from(parentDocElem.querySelectorAll('span.quote-inline'), span => span.remove()); + normalStatus.contentHtml = parentDocElem.children[1].innerHTML; + } } return normalStatus; -- 2.41.0 From 5be6a59f80d56f342d0276bdea4ec48f2d0d4c5b Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 25 Dec 2022 10:58:25 +0000 Subject: [PATCH 20/37] javascript: glitch: dont render cards if the status has a quote attached --- app/javascript/flavours/glitch/components/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 800832dc8e..24a616787b 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -687,7 +687,7 @@ class Status extends ImmutablePureComponent { if (!status.get('sensitive') && !(status.get('spoiler_text').length > 0) && settings.getIn(['collapsed', 'backgrounds', 'preview_images'])) { background = attachments.getIn([0, 'preview_url']); } - } else if (status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) { + } else if (!status.get('quote') && status.get('card') && settings.get('inline_preview_cards') && !this.props.muted) { media.push( Date: Sun, 25 Dec 2022 21:11:11 +0000 Subject: [PATCH 21/37] javascript: glitch: start rendering quotes --- .../glitch/components/status_content.js | 18 ++++++++++++++++++ .../glitch/styles/components/status.scss | 1 + 2 files changed, 19 insertions(+) diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index c618cedcab..ea5ac2b488 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -275,6 +275,21 @@ export default class StatusContent extends React.PureComponent { 'status__content--with-spoiler': status.get('spoiler_text').length > 0, }); + let quote = ''; + + if (status.get('quote', null) !== null) { + let quoteStatus = status.get('quote'); + let quoteStatusContent = { __html: quoteStatus.get('contentHtml') }; + + quote = ( +
+
+
+
+
+ ); + } + if (status.get('spoiler_text').length > 0) { let mentionsPlaceholder = ''; @@ -340,6 +355,7 @@ export default class StatusContent extends React.PureComponent { {mentionsPlaceholder}
+ {quote}
+ {quote}
+ {quote}
Date: Mon, 26 Dec 2022 00:59:04 +0000 Subject: [PATCH 22/37] flavors: glitch: show emojified display name in quotes --- .../flavours/glitch/components/status_content.js | 16 ++++++++++++++++ .../glitch/styles/components/status.scss | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index ea5ac2b488..282c455657 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -280,10 +280,26 @@ export default class StatusContent extends React.PureComponent { if (status.get('quote', null) !== null) { let quoteStatus = status.get('quote'); let quoteStatusContent = { __html: quoteStatus.get('contentHtml') }; + let quoteStatusAccount = quoteStatus.get('account'); + let quoteStatusDisplayName = { __html: quoteStatusAccount.get('display_name_html') }; + + console.log('QUOTE:', quoteStatus); + console.log('ACCOUNT:', quoteStatusAccount); quote = (
+ + + +
diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index cd2480d8a3..c23e9478dd 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -77,6 +77,10 @@ } } + .status__quote { + padding-bottom: 0.5em; + } + .status__quote, .status__content__text, .e-content { @@ -124,6 +128,11 @@ font-style: italic; } + i[role=img] { + font-style: normal; + padding-right: 0.25em; + } + sub { font-size: smaller; vertical-align: sub; -- 2.41.0 From adf1e9fc2e70ee8299fee6e965a15a5163ec367f Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 01:15:54 +0000 Subject: [PATCH 23/37] flavors: glitch: action bar: add quote button --- .../glitch/components/status_action_bar.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 977c98ccbc..2f0f55f507 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -25,6 +25,7 @@ const messages = defineMessages({ replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, + quote: { id: 'status.quote', defaultMessage: 'Quote' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, @@ -58,6 +59,7 @@ class StatusActionBar extends ImmutablePureComponent { onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, + onQuote: PropTypes.func, onDelete: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, @@ -124,6 +126,17 @@ class StatusActionBar extends ImmutablePureComponent { } } + handleQuoteClick = () => { + const { signedIn } = this.context.identity; + + if (signedIn) { + this.props.onQuote(this.props.status, this.context.router.history); + } else { + // TODO(ariadne): Add an interaction modal for quoting specifically. + this.props.onInteractionModal('reply', this.props.status); + } + } + handleBookmarkClick = (e) => { this.props.onBookmark(this.props.status, e); } @@ -307,6 +320,8 @@ class StatusActionBar extends ImmutablePureComponent { obfuscateCount /> + + {shareButton} -- 2.41.0 From 9d4851e3cd1fbda668cf276748171e1cdc4d77b3 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 01:19:47 +0000 Subject: [PATCH 24/37] glitch: actions: add quoteCompose and cancelQuoteCompose --- .../flavours/glitch/actions/compose.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 54909b56e4..2698ccad9d 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -82,6 +82,9 @@ export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS'; export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; +export const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; +export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL'; + const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, @@ -134,6 +137,25 @@ export function cancelReplyCompose() { }; }; +export function quoteCompose(status, router) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_QUOTE, + status: status, + }); + + if (!getState().getIn(['compose', 'mounted'])) { + router.push('/publish'); + } + }; +}; + +export function cancelQuoteCompose() { + return { + type: COMPOSE_QUOTE_CANCEL, + }; +}; + export function resetCompose() { return { type: COMPOSE_RESET, @@ -187,6 +209,7 @@ export function submitCompose(routerHistory) { status, content_type: getState().getIn(['compose', 'content_type']), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), + quote_id: getState().getIn(['compose', 'quote_id'], null), media_ids: media.map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), spoiler_text: spoilerText, -- 2.41.0 From c7e00d4c4e86d98e7030511e24ef4e466634aa71 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 01:25:11 +0000 Subject: [PATCH 25/37] flavors: glitch: add quote indicator component --- .../compose/components/quote_indicator.js | 82 +++++++++++++++++++ .../containers/quote_indicator_container.js | 27 ++++++ 2 files changed, 109 insertions(+) create mode 100644 app/javascript/flavours/glitch/features/compose/components/quote_indicator.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js diff --git a/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js b/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js new file mode 100644 index 0000000000..a3eeee1350 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js @@ -0,0 +1,82 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Components. +import AccountContainer from 'flavours/glitch/containers/account_container'; +import IconButton from 'flavours/glitch/components/icon_button'; +import AttachmentList from 'flavours/glitch/components/attachment_list'; + +// Messages. +const messages = defineMessages({ + cancel: { + defaultMessage: 'Cancel', + id: 'quote_indicator.cancel', + }, +}); + + +export default @injectIntl +class QuoteIndicator extends ImmutablePureComponent { + + static propTypes = { + status: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, + onCancel: PropTypes.func, + }; + + handleClick = () => { + const { onCancel } = this.props; + if (onCancel) { + onCancel(); + } + } + + // Rendering. + render () { + const { status, intl } = this.props; + + if (!status) { + return null; + } + + const account = status.get('account'); + const content = status.get('content'); + const attachments = status.get('media_attachments'); + + // The result. + return ( +
+
+ + {account && ( + + )} +
+
+ {attachments.size > 0 && ( + + )} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js new file mode 100644 index 0000000000..9421d69dd8 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { cancelQuoteCompose } from 'flavours/glitch/actions/compose'; +import QuoteIndicator from '../components/quote_indicator'; + +const makeMapStateToProps = () => { + const mapStateToProps = state => { + const statusId = state.getIn(['compose', 'quote_id']); + const editing = false; + + return { + status: state.getIn(['statuses', statusId]), + editing, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = dispatch => ({ + + onCancel () { + dispatch(cancelQuoteCompose()); + }, + +}); + +export default connect(makeMapStateToProps, mapDispatchToProps)(QuoteIndicator); -- 2.41.0 From a47d91707240dc6160ef00adb36f991d84acb1a5 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 01:31:57 +0000 Subject: [PATCH 26/37] flavors: glitch: add quote handling to status feature --- .../flavours/glitch/features/status/index.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index aaa9c7928f..1e9cafa241 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -23,6 +23,7 @@ import { } from 'flavours/glitch/actions/interactions'; import { replyCompose, + quoteCompose, mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; @@ -321,6 +322,20 @@ class Status extends ImmutablePureComponent { } } + handleQuoteClick = (status) => { + const { signedIn } = this.context.identity; + + if (signedIn) { + dispatch(quoteCompose(status, this.context.router.history)); + } else { + dispatch(openModal('INTERACTION', { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + })); + } + } + handleModalReblog = (status, privacy) => { const { dispatch } = this.props; @@ -679,6 +694,7 @@ class Status extends ImmutablePureComponent { onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} + onQuote={this.handleQuoteClick} onDelete={this.handleDeleteClick} onEdit={this.handleEditClick} onDirect={this.handleDirectClick} -- 2.41.0 From 766a6438113176b66aec14441cd6a410e43db236 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 01:41:32 +0000 Subject: [PATCH 27/37] add styles for quote indicator --- .../flavours/glitch/styles/components/compose_form.scss | 4 ++++ app/javascript/flavours/glitch/styles/components/status.scss | 1 + app/javascript/flavours/glitch/styles/contrast/diff.scss | 1 + .../flavours/glitch/styles/mastodon-light/diff.scss | 3 +++ 4 files changed, 9 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/components/compose_form.scss b/app/javascript/flavours/glitch/styles/components/compose_form.scss index 72d3aad1d3..00d395ce99 100644 --- a/app/javascript/flavours/glitch/styles/components/compose_form.scss +++ b/app/javascript/flavours/glitch/styles/components/compose_form.scss @@ -123,6 +123,7 @@ } } +.quote-indicator, .reply-indicator { margin: 0 0 10px; border-radius: 4px; @@ -133,6 +134,7 @@ flex: 0 2 auto; } +.quote-indicator__header, .reply-indicator__header { margin-bottom: 5px; overflow: hidden; @@ -140,11 +142,13 @@ & > .account.small { color: $inverted-text-color; } } +.quote-indicator__cancel, .reply-indicator__cancel { float: right; line-height: 24px; } +.quote-indicator__content, .reply-indicator__content { position: relative; margin: 10px 0; diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index c23e9478dd..b19c72cbef 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -696,6 +696,7 @@ } a.status__display-name, +.quote-indicator__display-name, .reply-indicator__display-name, .detailed-status__display-name, .account__display-name { diff --git a/app/javascript/flavours/glitch/styles/contrast/diff.scss b/app/javascript/flavours/glitch/styles/contrast/diff.scss index 4fa1a03616..97b7aa9fe5 100644 --- a/app/javascript/flavours/glitch/styles/contrast/diff.scss +++ b/app/javascript/flavours/glitch/styles/contrast/diff.scss @@ -14,6 +14,7 @@ .status__content a, .link-footer a, +.quote-indicator__content a, .reply-indicator__content a, .status__content__read-more-button { text-decoration: underline; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 6489c2f805..660ea707c9 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -257,6 +257,7 @@ html { } // Change the background colors of status__content__spoiler-link +.quote-indicator__content .status__content__spoiler-link, .reply-indicator__content .status__content__spoiler-link, .status__content .status__content__spoiler-link { background: $ui-base-color; @@ -662,6 +663,7 @@ html { } } +.quote-indicator, .reply-indicator { background: transparent; border: 1px solid lighten($ui-base-color, 8%); @@ -673,6 +675,7 @@ html { } .status__content, +.quote-indicator__content, .reply-indicator__content { a { color: $highlight-text-color; -- 2.41.0 From 214a4c9e6b81735da3b323269a0a7d17870929cf Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 01:41:45 +0000 Subject: [PATCH 28/37] glitch: reducers: set up correct state for quoting --- .../flavours/glitch/reducers/compose.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 1edc70add0..c67ac3f983 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -6,6 +6,8 @@ import { COMPOSE_REPLY, COMPOSE_REPLY_CANCEL, COMPOSE_DIRECT, + COMPOSE_QUOTE, + COMPOSE_QUOTE_CANCEL, COMPOSE_MENTION, COMPOSE_SUBMIT_REQUEST, COMPOSE_SUBMIT_SUCCESS, @@ -85,6 +87,7 @@ const initialState = ImmutableMap({ caretPosition: null, preselectDate: null, in_reply_to: null, + quote_id: null, is_submitting: false, is_uploading: false, is_changing_upload: false, @@ -173,6 +176,7 @@ function clearAll(state) { map.set('is_submitting', false); map.set('is_changing_upload', false); map.set('in_reply_to', null); + map.set('quote_id', null); map.update( 'advanced_options', map => map.mergeWith(overwrite, state.get('default_advanced_options')) @@ -410,6 +414,7 @@ export default function compose(state = initialState, action) { return state.withMutations(map => { map.set('id', null); map.set('in_reply_to', action.status.get('id')); + map.set('quote_id', null); map.set('text', statusToTextMentions(state, action.status)); map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); map.update( @@ -439,6 +444,26 @@ export default function compose(state = initialState, action) { }); case COMPOSE_REPLY_CANCEL: state = state.setIn(['advanced_options', 'threaded_mode'], false); + case COMPOSE_QUOTE: + return state.withMutations(map => { + map.set('id', null); + map.set('in_reply_to', null); + map.set('quote_id', action.status.get('id')); + map.set('text', ''); + map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy'))); + map.update( + 'advanced_options', + map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })) + ); + map.set('focusDate', new Date()); + map.set('caretPosition', null); + map.set('preselectDate', new Date()); + map.set('idempotencyKey', uuid()); + + console.log('COMPOSE_QUOTE, state:', map); + console.log('COMPOSE_QUOTE, action:', action); + }); + case COMPOSE_QUOTE_CANCEL: case COMPOSE_RESET: return state.withMutations(map => { map.set('in_reply_to', null); -- 2.41.0 From 5a8d4265ef7c64298b68fb7ba23efa88ac8b036d Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 02:53:01 +0000 Subject: [PATCH 29/37] glitch: fix up quote indicator --- .../flavours/glitch/components/status.js | 1 + .../glitch/containers/status_container.js | 20 +++++++++++++++++++ .../compose/components/compose_form.js | 2 ++ .../compose/components/quote_indicator.js | 6 +++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 24a616787b..e89cf246b5 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -69,6 +69,7 @@ class Status extends ImmutablePureComponent { status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, onReply: PropTypes.func, + onQuote: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, onBookmark: PropTypes.func, diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index c12b2e6143..9139d56d22 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -4,6 +4,7 @@ import { List as ImmutableList } from 'immutable'; import { makeGetStatus } from 'flavours/glitch/selectors'; import { replyCompose, + quoteCompose, mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; @@ -50,6 +51,8 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, + quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, @@ -111,6 +114,23 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }); }, + onQuote (status, router) { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.quoteMessage), + confirm: intl.formatMessage(messages.quoteConfirm), + onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), + onConfirm: () => dispatch(quoteCompose(status, router)), + })); + } else { + dispatch(quoteCompose(status, router)); + } + }); + }, + onModalReblog (status, privacy) { if (status.get('reblogged')) { dispatch(unreblog(status)); diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index 516648f4b3..f99a7363bb 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import QuoteIndicatorContainer from '../containers/quote_indicator_container'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestInput from '../../../components/autosuggest_input'; import { defineMessages, injectIntl } from 'react-intl'; @@ -309,6 +310,7 @@ class ComposeForm extends ImmutablePureComponent { +
+ {account && (
{attachments.size > 0 && ( -- 2.41.0 From e59c40eb686684c353e5b322753c5022b444a92b Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 02:57:20 +0000 Subject: [PATCH 30/37] activitypub: switch to fedibird:quoteUri --- app/helpers/context_helper.rb | 2 +- app/serializers/activitypub/note_serializer.rb | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index f4cf1f3a92..7e7dee7f9c 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -24,7 +24,7 @@ module ContextHelper voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, - quoteUrl: { 'quoteUrl' => 'as:quoteUrl' }, + quoteUri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, }.freeze def full_context diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index c9101b4469..dbde440467 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -11,8 +11,7 @@ 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 :quote_uri, if: -> { object.quote? } attribute :content attribute :content_map, if: :language? @@ -152,12 +151,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer end end - def quote_url + def quote_uri ActivityPub::TagManager.instance.uri_for(object.quote) if object.quote? end - alias misskey_quote quote_url - def local? object.account.local? end -- 2.41.0 From 7d4127065d5c53977eb0ac3411481e9b23cd2f96 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 03:57:02 +0000 Subject: [PATCH 31/37] formatting helper: add the quote-inline hack for incompatible clients --- app/helpers/formatting_helper.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 448177bec2..82cb4a5621 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -15,7 +15,17 @@ module FormattingHelper module_function :extract_status_plain_text def status_content_format(status) - html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type) + base = html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type) + + if status.quote? && status.local? + after_html = begin + "#{status.quote.to_log_permalink}" + end.html_safe # rubocop:disable Rails/OutputSafety + + base + after_html + else + base + end end def rss_status_content_format(status) -- 2.41.0 From 67a7b6067ade900187ee06e00c9835ae67e2c5b2 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 03:59:38 +0000 Subject: [PATCH 32/37] components: detailed status: suppress cards on quote posts --- .../glitch/features/status/components/detailed_status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 46770930f5..b53bf1d4ef 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -210,7 +210,7 @@ class DetailedStatus extends ImmutablePureComponent { ); mediaIcons.push('picture-o'); } - } else if (status.get('card')) { + } else if (!status.get('quote') && status.get('card')) { media.push(); mediaIcons.push('link'); } -- 2.41.0 From d23cd8da00a393cb276b07ba9412051bec363a47 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 04:09:05 +0000 Subject: [PATCH 33/37] add quote option to detailed statuses --- .../features/status/components/action_bar.js | 7 +++++++ .../containers/detailed_status_container.js | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index b6f8a98777..dd27599df9 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -18,6 +18,7 @@ const messages = defineMessages({ reply: { id: 'status.reply', defaultMessage: 'Reply' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, + quote: { id: 'status.quote', defaultMessage: 'Quote' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, @@ -52,6 +53,7 @@ class ActionBar extends React.PureComponent { onReblog: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired, onBookmark: PropTypes.func.isRequired, + onQuote: PropTypes.func.isRequired, onMute: PropTypes.func, onMuteConversation: PropTypes.func, onBlock: PropTypes.func, @@ -81,6 +83,10 @@ class ActionBar extends React.PureComponent { this.props.onBookmark(this.props.status, e); } + handleQuoteClick = (e) => { + this.props.onQuote(this.props.status); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -215,6 +221,7 @@ class ActionBar extends React.PureComponent {
+
{shareButton}
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index e5e0659876..087316401e 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -3,6 +3,7 @@ import DetailedStatus from '../components/detailed_status'; import { makeGetStatus } from 'flavours/glitch/selectors'; import { replyCompose, + quoteCompose, mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; @@ -33,6 +34,8 @@ import { showAlertForError } from 'flavours/glitch/actions/alerts'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, + quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, + quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, @@ -68,6 +71,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }); }, + onQuote (status, router) { + dispatch((_, getState) => { + let state = getState(); + if (state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.quoteMessage), + confirm: intl.formatMessage(messages.quoteConfirm), + onConfirm: () => dispatch(quoteCompose(status, router)), + })); + } else { + dispatch(quoteCompose(status, router)); + } + }); + }, + onModalReblog (status, privacy) { dispatch(reblog(status, privacy)); }, -- 2.41.0 From f0065720d677f58e7587bd769b2060f9f8b64eb8 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 04:17:21 +0000 Subject: [PATCH 34/37] javascript: fix dispatch --- .../flavours/glitch/features/status/components/action_bar.js | 2 +- app/javascript/flavours/glitch/features/status/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index dd27599df9..c0aae505a4 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -83,7 +83,7 @@ class ActionBar extends React.PureComponent { this.props.onBookmark(this.props.status, e); } - handleQuoteClick = (e) => { + handleQuoteClick = () => { this.props.onQuote(this.props.status); } diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 1e9cafa241..28023b6079 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -323,6 +323,7 @@ class Status extends ImmutablePureComponent { } handleQuoteClick = (status) => { + const { dispatch } = this.props; const { signedIn } = this.context.identity; if (signedIn) { -- 2.41.0 From 7efe4bc5d3df3bff0f57df15e76ee441b99858b5 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 04:26:52 +0000 Subject: [PATCH 35/37] activitypub: fix context extensions for quote_uri --- app/helpers/context_helper.rb | 2 +- app/serializers/activitypub/note_serializer.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 7e7dee7f9c..69d3be7527 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -24,7 +24,7 @@ module ContextHelper voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, - quoteUri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, + quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, }.freeze def full_context diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index dbde440467..dc810be956 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message, :quote_uri attributes :id, :type, :summary, :in_reply_to, :published, :url, -- 2.41.0 From df07456f51d0894a3dd35255c72edf8f44e6256d Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 26 Dec 2022 04:38:21 +0000 Subject: [PATCH 36/37] add reply and quote icons to the reply/quote indicators so people know what is going on --- .../glitch/features/compose/components/quote_indicator.js | 2 +- .../glitch/features/compose/components/reply_indicator.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js b/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js index 462c14fd3e..4fbe9e9085 100644 --- a/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js +++ b/app/javascript/flavours/glitch/features/compose/components/quote_indicator.js @@ -60,7 +60,7 @@ class QuoteIndicator extends ImmutablePureComponent { inverted /> {account && ( + {account && ( Date: Mon, 26 Dec 2022 04:38:32 +0000 Subject: [PATCH 37/37] delete obsolete console.log statements --- app/javascript/flavours/glitch/components/status_content.js | 3 --- app/javascript/flavours/glitch/reducers/compose.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 282c455657..7ae43b1552 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -283,9 +283,6 @@ export default class StatusContent extends React.PureComponent { let quoteStatusAccount = quoteStatus.get('account'); let quoteStatusDisplayName = { __html: quoteStatusAccount.get('display_name_html') }; - console.log('QUOTE:', quoteStatus); - console.log('ACCOUNT:', quoteStatusAccount); - quote = (
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index c67ac3f983..28ae0e47ee 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -459,9 +459,6 @@ export default function compose(state = initialState, action) { map.set('caretPosition', null); map.set('preselectDate', new Date()); map.set('idempotencyKey', uuid()); - - console.log('COMPOSE_QUOTE, state:', map); - console.log('COMPOSE_QUOTE, action:', action); }); case COMPOSE_QUOTE_CANCEL: case COMPOSE_RESET: -- 2.41.0