From b1a0322a06e6f8eca736132b1f7f2af8dc179afb Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 25 Apr 2019 02:47:33 +0200 Subject: [PATCH 01/33] Reject follow requests of blocked users (#10633) --- app/services/block_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/block_service.rb b/app/services/block_service.rb index 140b238df3e..10ed470e0a3 100644 --- a/app/services/block_service.rb +++ b/app/services/block_service.rb @@ -6,6 +6,7 @@ class BlockService < BaseService UnfollowService.new.call(account, target_account) if account.following?(target_account) UnfollowService.new.call(target_account, account) if target_account.following?(account) + RejectFollowService.new.call(account, target_account) if target_account.requested?(account) block = account.block!(target_account) From 852ccea676bbb8a91a0d1bc66570e68e6de2c9e1 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 25 Apr 2019 02:48:54 +0200 Subject: [PATCH 02/33] Fix upload progressbar when image resizing is involved (#10632) --- app/javascript/mastodon/actions/compose.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index d65d4104826..0ee663766ab 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -203,8 +203,8 @@ export function uploadCompose(files) { return function (dispatch, getState) { const uploadLimit = 4; const media = getState().getIn(['compose', 'media_attachments']); - const total = Array.from(files).reduce((a, v) => a + v.size, 0); const progress = new Array(files.length).fill(0); + let total = Array.from(files).reduce((a, v) => a + v.size, 0); if (files.length + media.size > uploadLimit) { dispatch(showAlert(undefined, messages.uploadErrorLimit)); @@ -224,6 +224,8 @@ export function uploadCompose(files) { resizeImage(f).then(file => { const data = new FormData(); data.append('file', file); + // Account for disparity in size of original image and resized data + total += file.size - f.size; return api(getState).post('/api/v1/media', data, { onUploadProgress: function({ loaded }){ From f27d7093513c0265010d019adc01b3f7ea02ef47 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Apr 2019 02:49:06 +0200 Subject: [PATCH 03/33] Fix not being able to save e-mail preference for new pending accounts (#10622) --- app/controllers/settings/notifications_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb index da8a03d9649..b2ce83e4219 100644 --- a/app/controllers/settings/notifications_controller.rb +++ b/app/controllers/settings/notifications_controller.rb @@ -25,7 +25,7 @@ class Settings::NotificationsController < Settings::BaseController def user_settings_params params.require(:user).permit( - notification_emails: %i(follow follow_request reblog favourite mention digest report), + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end From e451ba0e837eb5b3d4f7fe75ca3e16680afaf129 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 25 Apr 2019 02:49:25 +0200 Subject: [PATCH 04/33] Fix LDAP/PAM/SAML/CAS users not being approved instantly (#10621) --- app/models/concerns/ldap_authenticable.rb | 1 + app/models/concerns/omniauthable.rb | 1 + app/models/concerns/pam_authenticable.rb | 1 + app/models/user.rb | 7 ++++++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/ldap_authenticable.rb b/app/models/concerns/ldap_authenticable.rb index e1b5e3832b4..84ff84c4b05 100644 --- a/app/models/concerns/ldap_authenticable.rb +++ b/app/models/concerns/ldap_authenticable.rb @@ -6,6 +6,7 @@ module LdapAuthenticable def ldap_setup(_attributes) self.confirmed_at = Time.now.utc self.admin = false + self.external = true save! end diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index 1b28b8162ef..28303308395 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -66,6 +66,7 @@ module Omniauthable email: email || "#{TEMP_EMAIL_PREFIX}-#{auth.uid}-#{auth.provider}.com", password: Devise.friendly_token[0, 20], agreement: true, + external: true, account_attributes: { username: ensure_unique_username(auth.uid), display_name: display_name, diff --git a/app/models/concerns/pam_authenticable.rb b/app/models/concerns/pam_authenticable.rb index 2f651c1a359..6169d4dfaa1 100644 --- a/app/models/concerns/pam_authenticable.rb +++ b/app/models/concerns/pam_authenticable.rb @@ -34,6 +34,7 @@ module PamAuthenticable self.confirmed_at = Time.now.utc self.admin = false self.account = account + self.external = true account.destroy! unless save end diff --git a/app/models/user.rb b/app/models/user.rb index 135baae122b..9a067100624 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -107,6 +107,7 @@ class User < ApplicationRecord :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code + attr_writer :external def confirmed? confirmed_at.present? @@ -273,13 +274,17 @@ class User < ApplicationRecord private def set_approved - self.approved = open_registrations? || invited? + self.approved = open_registrations? || invited? || external? end def open_registrations? Setting.registrations_mode == 'open' end + def external? + @external + end + def sanitize_languages return if chosen_languages.nil? chosen_languages.reject!(&:blank?) From 8f4c34669fc3117ee7526f606a7a3fb7f80ab5cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" Date: Thu, 25 Apr 2019 13:36:56 +0200 Subject: [PATCH 05/33] Bump bootsnap from 1.4.3 to 1.4.4 (#10634) Bumps [bootsnap](https://github.com/Shopify/bootsnap) from 1.4.3 to 1.4.4. - [Release notes](https://github.com/Shopify/bootsnap/releases) - [Changelog](https://github.com/Shopify/bootsnap/blob/master/CHANGELOG.md) - [Commits](https://github.com/Shopify/bootsnap/compare/v1.4.3...v1.4.4) Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index da984ccae0c..66fc8d1f4bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,7 +99,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.4.3) + bootsnap (1.4.4) msgpack (~> 1.0) brakeman (4.5.0) browser (2.5.3) @@ -346,7 +346,7 @@ GEM mini_mime (1.0.1) mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.9) + msgpack (1.2.10) multi_json (1.13.1) multipart-post (2.0.0) necromancer (0.4.0) From c008911249a2fc0efaf22b83e51ea8510e67acac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C4=BDach?= Date: Fri, 26 Apr 2019 18:07:36 +0200 Subject: [PATCH 06/33] New string added for Slovak translation (#10637) --- config/locales/sk.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/locales/sk.yml b/config/locales/sk.yml index d3580c98190..b6a966fa7d1 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -527,16 +527,17 @@ sk: login: Prihlás sa logout: Odhlás sa migrate_account: Presúvam sa na iný účet - migrate_account_html: Pokiaľ si želáš presmerovať tento účet na nejaký iný, môžeš si to nastaviť tu. - or_log_in_with: Alebo prihlásiť z + migrate_account_html: Ak si želáš presmerovať tento účet na nejaký iný, môžeš si to nastaviť tu. + or_log_in_with: Alebo prihlás s providers: cas: CAS saml: SAML register: Zaregistruj sa - resend_confirmation: Poslať potvrdzujúce pokyny znovu + resend_confirmation: Zašli potvrdzujúce pokyny znovu reset_password: Obnov heslo security: Zabezpečenie set_new_password: Nastav nové heslo + trouble_logging_in: Problém s prihlásením? authorize_follow: already_following: Tento účet už následuješ error: Naneštastie nastala chyba pri hľadaní vzdialeného účtu From fba96c808d25d2fc35ec63ee6745a1e55a95d707 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 27 Apr 2019 03:24:09 +0200 Subject: [PATCH 07/33] Add blurhash (#10630) * Add blurhash * Use fallback color for spoiler when blurhash missing * Federate the blurhash and accept it as long as it's at most 5x5 * Display unknown media attachments as blurhash placeholders * Improve style of embed actions and spoiler button * Change blurhash resolution from 3x3 to 4x4 * Improve dependency definitions * Fix code style issues --- Gemfile | 1 + Gemfile.lock | 3 + .../mastodon/components/media_gallery.js | 96 +++++++++++++------ app/javascript/mastodon/components/status.js | 3 +- .../report/components/status_check_box.js | 1 + .../status/components/detailed_status.js | 6 +- .../features/ui/components/media_modal.js | 1 + .../features/ui/components/video_modal.js | 1 + .../mastodon/features/video/index.js | 49 ++++++++-- .../styles/mastodon/components.scss | 70 ++++++++++++-- app/lib/activitypub/activity/create.rb | 7 +- app/lib/activitypub/adapter.rb | 1 + app/models/media_attachment.rb | 15 ++- .../activitypub/note_serializer.rb | 4 +- .../rest/media_attachment_serializer.rb | 2 +- .../stream_entries/_detailed_status.html.haml | 2 +- .../stream_entries/_simple_status.html.haml | 2 +- ...25523_add_blurhash_to_media_attachments.rb | 5 + db/schema.rb | 3 +- lib/paperclip/blurhash_transcoder.rb | 16 ++++ package.json | 1 + yarn.lock | 5 + 22 files changed, 234 insertions(+), 60 deletions(-) create mode 100644 db/migrate/20190420025523_add_blurhash_to_media_attachments.rb create mode 100644 lib/paperclip/blurhash_transcoder.rb diff --git a/Gemfile b/Gemfile index 6fe97412b82..fa8478d8972 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' gem 'paperclip-av-transcoder', '~> 0.6' gem 'streamio-ffmpeg', '~> 3.0' +gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.6' diff --git a/Gemfile.lock b/Gemfile.lock index 66fc8d1f4bf..0148cb5ea98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,6 +99,8 @@ GEM rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) + blurhash (0.1.2) + ffi (~> 1.10.0) bootsnap (1.4.4) msgpack (~> 1.0) brakeman (4.5.0) @@ -661,6 +663,7 @@ DEPENDENCIES aws-sdk-s3 (~> 1.36) better_errors (~> 2.5) binding_of_caller (~> 0.7) + blurhash (~> 0.1) bootsnap (~> 1.4) brakeman (~> 4.5) browser diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index a2bc9525564..f548296d069 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -7,6 +7,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from '../is_mobile'; import classNames from 'classnames'; import { autoPlayGif, displayMedia } from '../initial_state'; +import { decode } from 'blurhash'; const messages = defineMessages({ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, @@ -21,6 +22,7 @@ class Item extends React.PureComponent { size: PropTypes.number.isRequired, onClick: PropTypes.func.isRequired, displayWidth: PropTypes.number, + visible: PropTypes.bool.isRequired, }; static defaultProps = { @@ -29,6 +31,10 @@ class Item extends React.PureComponent { size: 1, }; + state = { + loaded: false, + }; + handleMouseEnter = (e) => { if (this.hoverToPlay()) { e.target.play(); @@ -62,8 +68,40 @@ class Item extends React.PureComponent { e.stopPropagation(); } + componentDidMount () { + if (this.props.attachment.get('blurhash')) { + this._decode(); + } + } + + componentDidUpdate (prevProps) { + if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) { + this._decode(); + } + } + + _decode () { + const hash = this.props.attachment.get('blurhash'); + const pixels = decode(hash, 32, 32); + + if (pixels) { + const ctx = this.canvas.getContext('2d'); + const imageData = new ImageData(pixels, 32, 32); + + ctx.putImageData(imageData, 0, 0); + } + } + + setCanvasRef = c => { + this.canvas = c; + } + + handleImageLoad = () => { + this.setState({ loaded: true }); + } + render () { - const { attachment, index, size, standalone, displayWidth } = this.props; + const { attachment, index, size, standalone, displayWidth, visible } = this.props; let width = 50; let height = 100; @@ -116,12 +154,20 @@ class Item extends React.PureComponent { let thumbnail = ''; - if (attachment.get('type') === 'image') { + if (attachment.get('type') === 'unknown') { + return ( +
+ + + +
+ ); + } else if (attachment.get('type') === 'image') { const previewUrl = attachment.get('preview_url'); const previewWidth = attachment.getIn(['meta', 'small', 'width']); - const originalUrl = attachment.get('url'); - const originalWidth = attachment.getIn(['meta', 'original', 'width']); + const originalUrl = attachment.get('url'); + const originalWidth = attachment.getIn(['meta', 'original', 'width']); const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; @@ -147,6 +193,7 @@ class Item extends React.PureComponent { alt={attachment.get('description')} title={attachment.get('description')} style={{ objectPosition: `${x}% ${y}%` }} + onLoad={this.handleImageLoad} /> ); @@ -176,7 +223,8 @@ class Item extends React.PureComponent { return (
- {thumbnail} + + {visible && thumbnail}
); } @@ -225,6 +273,7 @@ class MediaGallery extends React.PureComponent { if (node /*&& this.isStandaloneEligible()*/) { // offsetWidth triggers a layout, so only calculate when we need to if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); + this.setState({ width: node.offsetWidth, }); @@ -242,7 +291,7 @@ class MediaGallery extends React.PureComponent { const width = this.state.width || defaultWidth; - let children; + let children, spoilerButton; const style = {}; @@ -256,35 +305,28 @@ class MediaGallery extends React.PureComponent { style.height = height; } - if (!visible) { - let warning; + const size = media.take(4).size; - if (sensitive) { - warning = ; - } else { - warning = ; - } + if (this.isStandaloneEligible()) { + children = ; + } else { + children = media.take(4).map((attachment, i) => ); + } - children = ( - ); - } else { - const size = media.take(4).size; - - if (this.isStandaloneEligible()) { - children = ; - } else { - children = media.take(4).map((attachment, i) => ); - } } return (
-
- +
+ {spoilerButton}
{children} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index cea9a0c2e55..95ca4a5485d 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -274,7 +274,7 @@ class Status extends ImmutablePureComponent { if (status.get('poll')) { media = ; } else if (status.get('media_attachments').size > 0) { - if (this.props.muted || status.get('media_attachments').some(item => item.get('type') === 'unknown')) { + if (this.props.muted) { media = ( ( ( ; } else if (status.get('media_attachments').size > 0) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - media = ; - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + if (status.getIn(['media_attachments', 0, 'type']) === 'video') { const video = status.getIn(['media_attachments', 0]); media = (