diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 0b1fd365238..48970f8bd7e 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2412,7 +2412,7 @@ a.account__display-name {
& > div {
background: rgba($base-shadow-color, 0.6);
- border-radius: 4px;
+ border-radius: 8px;
padding: 12px 9px;
flex: 0 0 auto;
display: flex;
@@ -2423,19 +2423,18 @@ a.account__display-name {
button,
a {
display: inline;
- color: $primary-text-color;
+ color: $secondary-text-color;
background: transparent;
border: 0;
- padding: 0 5px;
+ padding: 0 8px;
text-decoration: none;
- opacity: 0.6;
font-size: 18px;
line-height: 18px;
&:hover,
&:active,
&:focus {
- opacity: 1;
+ color: $primary-text-color;
}
}
@@ -2932,15 +2931,49 @@ a.status-card.compact:hover {
}
.spoiler-button {
- display: none;
- left: 4px;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
position: absolute;
- text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
- top: 4px;
z-index: 100;
- &.spoiler-button--visible {
+ &--minified {
display: block;
+ left: 4px;
+ top: 4px;
+ width: auto;
+ height: auto;
+ }
+
+ &--hidden {
+ display: none;
+ }
+
+ &__overlay {
+ display: block;
+ background: transparent;
+ width: 100%;
+ height: 100%;
+ border: 0;
+
+ &__label {
+ display: inline-block;
+ background: rgba($base-overlay-background, 0.5);
+ border-radius: 8px;
+ padding: 8px 12px;
+ color: $primary-text-color;
+ font-weight: 500;
+ font-size: 14px;
+ }
+
+ &:hover,
+ &:focus,
+ &:active {
+ .spoiler-button__overlay__label {
+ background: rgba($base-overlay-background, 0.8);
+ }
+ }
}
}
@@ -4313,6 +4346,8 @@ a.status-card.compact:hover {
text-decoration: none;
color: $secondary-text-color;
line-height: 0;
+ position: relative;
+ z-index: 1;
&,
img {
@@ -4325,6 +4360,21 @@ a.status-card.compact:hover {
}
}
+.media-gallery__preview {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ background: $base-overlay-background;
+
+ &--hidden {
+ display: none;
+ }
+}
+
.media-gallery__gifv {
height: 100%;
overflow: hidden;
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index dabdcbcf7b2..6b16c998633 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -194,7 +194,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
next if attachment['url'].blank?
href = Addressable::URI.parse(attachment['url']).normalize.to_s
- media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'])
+ media_attachment = MediaAttachment.create(account: @account, remote_url: href, description: attachment['name'].presence, focus: attachment['focalPoint'], blurhash: supported_blurhash?(attachment['blurhash']) ? attachment['blurhash'] : nil)
media_attachments << media_attachment
next if unsupported_media_type?(attachment['mediaType']) || skip_download?
@@ -369,6 +369,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
end
+ def supported_blurhash?(blurhash)
+ components = blurhash.blank? ? nil : Blurhash.components(blurhash)
+ components.present? && components.none? { |comp| comp > 5 }
+ end
+
def skip_download?
return @skip_download if defined?(@skip_download)
@skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index 94eb2899c1b..c259c96f410 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -19,6 +19,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
+ blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
}.freeze
def self.default_key_transform
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index a57ba0b2ea0..ab794faa055 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -18,6 +18,7 @@
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
+# blurhash :string
#
class MediaAttachment < ApplicationRecord
@@ -32,6 +33,11 @@ class MediaAttachment < ApplicationRecord
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4', 'video/quicktime'].freeze
VIDEO_CONVERTIBLE_MIME_TYPES = ['video/webm', 'video/quicktime'].freeze
+ BLURHASH_OPTIONS = {
+ x_comp: 4,
+ y_comp: 4,
+ }.freeze
+
IMAGE_STYLES = {
original: {
pixels: 1_638_400, # 1280x1280px
@@ -41,6 +47,7 @@ class MediaAttachment < ApplicationRecord
small: {
pixels: 160_000, # 400x400px
file_geometry_parser: FastGeometryParser,
+ blurhash: BLURHASH_OPTIONS,
},
}.freeze
@@ -53,6 +60,8 @@ class MediaAttachment < ApplicationRecord
},
format: 'png',
time: 0,
+ file_geometry_parser: FastGeometryParser,
+ blurhash: BLURHASH_OPTIONS,
},
}.freeze
@@ -166,11 +175,11 @@ class MediaAttachment < ApplicationRecord
def file_processors(f)
if f.file_content_type == 'image/gif'
- [:gif_transcoder]
+ [:gif_transcoder, :blurhash_transcoder]
elsif VIDEO_MIME_TYPES.include? f.file_content_type
- [:video_transcoder]
+ [:video_transcoder, :blurhash_transcoder]
else
- [:lazy_thumbnail]
+ [:lazy_thumbnail, :blurhash_transcoder]
end
end
end
diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb
index d11cfa59aee..67f596e78a6 100644
--- a/app/serializers/activitypub/note_serializer.rb
+++ b/app/serializers/activitypub/note_serializer.rb
@@ -2,7 +2,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer
context_extensions :atom_uri, :conversation, :sensitive,
- :hashtag, :emoji, :focal_point
+ :hashtag, :emoji, :focal_point, :blurhash
attributes :id, :type, :summary,
:in_reply_to, :published, :url,
@@ -153,7 +153,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
class MediaAttachmentSerializer < ActivityPub::Serializer
include RoutingHelper
- attributes :type, :media_type, :url, :name
+ attributes :type, :media_type, :url, :name, :blurhash
attribute :focal_point, if: :focal_point?
def type
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
index 51011788bae..1b3498ea4eb 100644
--- a/app/serializers/rest/media_attachment_serializer.rb
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -5,7 +5,7 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer
attributes :id, :type, :url, :preview_url,
:remote_url, :text_url, :meta,
- :description
+ :description, :blurhash
def id
object.id.to_s
diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml
index 4459581d94c..23f2920d89c 100644
--- a/app/views/stream_entries/_detailed_status.html.haml
+++ b/app/views/stream_entries/_detailed_status.html.haml
@@ -28,7 +28,7 @@
- elsif !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), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
+ = 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: 670, height: 380, detailed: true, inline: true, alt: video.description do
= render partial: 'stream_entries/attachment_list', locals: { attachments: status.media_attachments }
- else
= react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml
index ba22c534074..0df7497e10b 100644
--- a/app/views/stream_entries/_simple_status.html.haml
+++ b/app/views/stream_entries/_simple_status.html.haml
@@ -32,7 +32,7 @@
- elsif !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), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
+ = 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 do
= render partial: 'stream_entries/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 || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
diff --git a/db/migrate/20190420025523_add_blurhash_to_media_attachments.rb b/db/migrate/20190420025523_add_blurhash_to_media_attachments.rb
new file mode 100644
index 00000000000..f2bbe0a856c
--- /dev/null
+++ b/db/migrate/20190420025523_add_blurhash_to_media_attachments.rb
@@ -0,0 +1,5 @@
+class AddBlurhashToMediaAttachments < ActiveRecord::Migration[5.2]
+ def change
+ add_column :media_attachments, :blurhash, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3060159d25d..8613539d644 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_04_09_054914) do
+ActiveRecord::Schema.define(version: 2019_04_20_025523) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -362,6 +362,7 @@ ActiveRecord::Schema.define(version: 2019_04_09_054914) do
t.bigint "account_id"
t.text "description"
t.bigint "scheduled_status_id"
+ t.string "blurhash"
t.index ["account_id"], name: "index_media_attachments_on_account_id"
t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id"
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true
diff --git a/lib/paperclip/blurhash_transcoder.rb b/lib/paperclip/blurhash_transcoder.rb
new file mode 100644
index 00000000000..08925a6dde0
--- /dev/null
+++ b/lib/paperclip/blurhash_transcoder.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Paperclip
+ class BlurhashTranscoder < Paperclip::Processor
+ def make
+ return @file unless options[:style] == :small
+
+ pixels = convert(':source RGB:-', source: File.expand_path(@file.path)).unpack('C*')
+ geometry = options.fetch(:file_geometry_parser).from_file(@file)
+
+ attachment.instance.blurhash = Blurhash.encode(geometry.width, geometry.height, pixels, options[:blurhash] || {})
+
+ @file
+ end
+ end
+end
diff --git a/package.json b/package.json
index 63cfa25b805..67396ccc379 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"babel-plugin-react-intl": "^3.0.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"babel-runtime": "^6.26.0",
+ "blurhash": "^1.0.0",
"classnames": "^2.2.5",
"compression-webpack-plugin": "^2.0.0",
"cross-env": "^5.1.4",
diff --git a/yarn.lock b/yarn.lock
index 9d7f0eccb55..329216cdb9a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1743,6 +1743,11 @@ bluebird@^3.5.1, bluebird@^3.5.3:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
+blurhash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.0.0.tgz#9087bc5cc4d482f1305059d7410df4133adcab2e"
+ integrity sha512-x6fpZnd6AWde4U9m7xhUB44qIvGV4W6OdTAXGabYm4oZUOOGh5K1HAEoGAQn3iG4gbbPn9RSGce3VfNgGsX/Vw==
+
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"