From b1a0322a06e6f8eca736132b1f7f2af8dc179afb Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Thu, 25 Apr 2019 02:47:33 +0200 Subject: [PATCH 01/54] 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 140b238df3..10ed470e0a 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 <thib@sitedethib.com> Date: Thu, 25 Apr 2019 02:48:54 +0200 Subject: [PATCH 02/54] 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 d65d410482..0ee663766a 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 <eugen@zeonfederated.com> Date: Thu, 25 Apr 2019 02:49:06 +0200 Subject: [PATCH 03/54] 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 da8a03d964..b2ce83e421 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 <eugen@zeonfederated.com> Date: Thu, 25 Apr 2019 02:49:25 +0200 Subject: [PATCH 04/54] 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 e1b5e3832b..84ff84c4b0 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 1b28b8162e..2830330839 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 2f651c1a35..6169d4dfaa 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 135baae122..9a06710062 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]" <dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2019 13:36:56 +0200 Subject: [PATCH 05/54] 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] <support@dependabot.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index da984ccae0..66fc8d1f4b 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?= <mareklachbc@tutanota.com> Date: Fri, 26 Apr 2019 18:07:36 +0200 Subject: [PATCH 06/54] 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 d3580c9819..b6a966fa7d 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 <a href="%{path}">nastaviť tu</a>. - or_log_in_with: Alebo prihlásiť z + migrate_account_html: Ak si želáš presmerovať tento účet na nejaký iný, môžeš si to <a href="%{path}">nastaviť tu</a>. + 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 <eugen@zeonfederated.com> Date: Sat, 27 Apr 2019 03:24:09 +0200 Subject: [PATCH 07/54] 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 6fe97412b8..fa8478d897 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 66fc8d1f4b..0148cb5ea9 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 a2bc952556..f548296d06 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 ( + <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} > + <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> + </a> + </div> + ); + } 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} /> </a> ); @@ -176,7 +223,8 @@ class Item extends React.PureComponent { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - {thumbnail} + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} /> + {visible && thumbnail} </div> ); } @@ -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 = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; - } else { - warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; - } + if (this.isStandaloneEligible()) { + children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; + } else { + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible} />); + } - children = ( - <button type='button' className='media-spoiler' onClick={this.handleOpen} style={style} ref={this.handleRef}> - <span className='media-spoiler__warning'>{warning}</span> - <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> + if (visible) { + spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />; + } else { + spoilerButton = ( + <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> + <span className='spoiler-button__overlay__label'>{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}</span> </button> ); - } else { - const size = media.take(4).size; - - if (this.isStandaloneEligible()) { - children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} />; - } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} />); - } } return ( <div className='media-gallery' style={style} ref={this.handleRef}> - <div className={classNames('spoiler-button', { 'spoiler-button--visible': visible })}> - <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} /> + <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}> + {spoilerButton} </div> {children} diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index cea9a0c2e5..95ca4a5485 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 = <PollContainer pollId={status.get('poll')} />; } 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 = ( <AttachmentList compact @@ -289,6 +289,7 @@ class Status extends ImmutablePureComponent { {Component => ( <Component preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} width={this.props.cachedMediaWidth} diff --git a/app/javascript/mastodon/features/report/components/status_check_box.js b/app/javascript/mastodon/features/report/components/status_check_box.js index 2552d94d89..c29e517da8 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.js +++ b/app/javascript/mastodon/features/report/components/status_check_box.js @@ -35,6 +35,7 @@ export default class StatusCheckBox extends React.PureComponent { {Component => ( <Component preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} width={239} diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 5c79f9f197..84471f9a3f 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -5,7 +5,6 @@ import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; import StatusContent from '../../../components/status_content'; import MediaGallery from '../../../components/media_gallery'; -import AttachmentList from '../../../components/attachment_list'; import { Link } from 'react-router-dom'; import { FormattedDate, FormattedNumber } from 'react-intl'; import Card from './card'; @@ -109,14 +108,13 @@ export default class DetailedStatus extends ImmutablePureComponent { if (status.get('poll')) { media = <PollContainer pollId={status.get('poll')} />; } else if (status.get('media_attachments').size > 0) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - media = <AttachmentList media={status.get('media_attachments')} />; - } 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 = ( <Video preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} width={300} diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 2120746da3..848cb20b3c 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -144,6 +144,7 @@ class MediaModal extends ImmutablePureComponent { return ( <Video preview={image.get('preview_url')} + blurhash={image.get('blurhash')} src={image.get('url')} width={image.get('width')} height={image.get('height')} diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 7cf3eb4d45..52457a630b 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -20,6 +20,7 @@ export default class VideoModal extends ImmutablePureComponent { <div> <Video preview={media.get('preview_url')} + blurhash={media.get('blurhash')} src={media.get('url')} startTime={time} onCloseVideo={onClose} diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 55dd249e14..7b6113e6a4 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -7,6 +7,7 @@ import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { displayMedia } from '../../initial_state'; import Icon from 'mastodon/components/icon'; +import { decode } from 'blurhash'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -102,6 +103,7 @@ class Video extends React.PureComponent { inline: PropTypes.bool, cacheWidth: PropTypes.func, intl: PropTypes.object.isRequired, + blurhash: PropTypes.string, }; state = { @@ -139,6 +141,7 @@ class Video extends React.PureComponent { setVideoRef = c => { this.video = c; + if (this.video) { this.setState({ volume: this.video.volume, muted: this.video.muted }); } @@ -152,6 +155,10 @@ class Video extends React.PureComponent { this.volume = c; } + setCanvasRef = c => { + this.canvas = c; + } + handleClickRoot = e => e.stopPropagation(); handlePlay = () => { @@ -170,7 +177,6 @@ class Video extends React.PureComponent { } handleVolumeMouseDown = e => { - document.addEventListener('mousemove', this.handleMouseVolSlide, true); document.addEventListener('mouseup', this.handleVolumeMouseUp, true); document.addEventListener('touchmove', this.handleMouseVolSlide, true); @@ -190,7 +196,6 @@ class Video extends React.PureComponent { } handleMouseVolSlide = throttle(e => { - const rect = this.volume.getBoundingClientRect(); const x = (e.clientX - rect.left) / this.volWidth; //x position within the element. @@ -261,6 +266,10 @@ class Video extends React.PureComponent { document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); + + if (this.props.blurhash) { + this._decode(); + } } componentWillUnmount () { @@ -270,6 +279,24 @@ class Video extends React.PureComponent { document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); } + componentDidUpdate (prevProps) { + if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) { + this._decode(); + } + } + + _decode () { + const hash = this.props.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); + } + } + handleFullscreenChange = () => { this.setState({ fullscreen: isFullscreen() }); } @@ -314,6 +341,7 @@ class Video extends React.PureComponent { handleOpenVideo = () => { const { src, preview, width, height, alt } = this.props; + const media = fromJS({ type: 'video', url: src, @@ -351,6 +379,7 @@ class Video extends React.PureComponent { } let preload; + if (startTime || fullscreen || dragging) { preload = 'auto'; } else if (detailed) { @@ -360,6 +389,7 @@ class Video extends React.PureComponent { } let warning; + if (sensitive) { warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; } else { @@ -377,7 +407,9 @@ class Video extends React.PureComponent { onClick={this.handleClickRoot} tabIndex={0} > - <video + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} /> + + {revealed && <video ref={this.setVideoRef} src={src} poster={preview} @@ -397,12 +429,13 @@ class Video extends React.PureComponent { onLoadedData={this.handleLoadedData} onProgress={this.handleProgress} onVolumeChange={this.handleVolumeChange} - /> + />} - <button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> - <span className='video-player__spoiler__title'>{warning}</span> - <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </button> + <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}> + <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> + <span className='spoiler-button__overlay__label'>{warning}</span> + </button> + </div> <div className={classNames('video-player__controls', { active: paused || hovered })}> <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0b1fd36523..48970f8bd7 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 dabdcbcf7b..6b16c99863 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 94eb2899c1..c259c96f41 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 a57ba0b2ea..ab794faa05 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 d11cfa59ae..67f596e78a 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 51011788ba..1b3498ea4e 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 4459581d94..23f2920d89 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 ba22c53407..0df7497e10 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 0000000000..f2bbe0a856 --- /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 3060159d25..8613539d64 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 0000000000..08925a6dde --- /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 63cfa25b80..67396ccc37 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 9d7f0eccb5..329216cdb9 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" From e18786dec769791d6ecd7f21a89ae97fb00c44cf Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Sat, 27 Apr 2019 23:55:16 +0200 Subject: [PATCH 08/54] Fix approved column being set to nil instead of false (#10642) Fix https://github.com/tootsuite/mastodon/pull/10621#issuecomment-487316619 --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 9a06710062..c42f6ad8d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -282,7 +282,7 @@ class User < ApplicationRecord end def external? - @external + !!@external end def sanitize_languages From 5e79dd3f174524ebc1e4f0379de4c5bbe7db4b32 Mon Sep 17 00:00:00 2001 From: partev <petrosyan@gmail.com> Date: Sat, 27 Apr 2019 23:51:20 -0400 Subject: [PATCH 09/54] Update hy.json (#10644) --- app/javascript/mastodon/locales/hy.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index d155619c9f..ca7732d851 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -243,7 +243,7 @@ "navigation_bar.pins": "Ամրացված թթեր", "navigation_bar.preferences": "Նախապատվություններ", "navigation_bar.public_timeline": "Դաշնային հոսք", - "navigation_bar.security": "Security", + "navigation_bar.security": "Անվտանգություն", "notification.favourite": "{name} հավանեց թութդ", "notification.follow": "{name} սկսեց հետեւել քեզ", "notification.mention": "{name} նշեց քեզ", @@ -309,7 +309,7 @@ "search_results.accounts": "People", "search_results.hashtags": "Hashtags", "search_results.statuses": "Toots", - "search_results.total": "{count, number} {count, plural, one {result} other {results}}", + "search_results.total": "{count, number} {count, plural, one {արդյունք} other {արդյունք}}", "status.admin_account": "Open moderation interface for @{name}", "status.admin_status": "Open this status in the moderation interface", "status.block": "Արգելափակել @{name}֊ին", From feff0fc9b2cf0a37175da23c38484759971b8db8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2019 18:05:22 +0200 Subject: [PATCH 10/54] Bump rubocop from 0.67.2 to 0.68.0 (#10654) Bumps [rubocop](https://github.com/rubocop-hq/rubocop) from 0.67.2 to 0.68.0. - [Release notes](https://github.com/rubocop-hq/rubocop/releases) - [Changelog](https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop-hq/rubocop/compare/v0.67.2...v0.68.0) Signed-off-by: dependabot[bot] <support@dependabot.com> --- Gemfile | 2 +- Gemfile.lock | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index fa8478d897..b908dc4945 100644 --- a/Gemfile +++ b/Gemfile @@ -128,7 +128,7 @@ group :development do gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' - gem 'rubocop', '~> 0.67', require: false + gem 'rubocop', '~> 0.68', require: false gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false gem 'scss_lint', '~> 0.57', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0148cb5ea9..d03ed2e607 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -395,7 +395,7 @@ GEM parallel (1.17.0) parallel_tests (2.28.0) parallel - parser (2.6.2.1) + parser (2.6.3.0) ast (~> 2.4.0) pastel (0.7.2) equatable (~> 0.5.0) @@ -420,7 +420,6 @@ GEM pry (~> 0.10) pry-rails (0.3.9) pry (>= 0.10.4) - psych (3.1.0) public_suffix (3.0.3) puma (3.12.1) pundit (2.0.1) @@ -528,11 +527,10 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.8.0) - rubocop (0.67.2) + rubocop (0.68.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) - psych (>= 3.1.0) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.6) @@ -750,7 +748,7 @@ DEPENDENCIES rqrcode (~> 0.10) rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) - rubocop (~> 0.67) + rubocop (~> 0.68) sanitize (~> 5.0) scss_lint (~> 0.57) sidekiq (~> 5.2) From 7c94b190c8b65da46bb394499240c4c8569d29ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2019 18:05:49 +0200 Subject: [PATCH 11/54] Bump bullet from 5.9.0 to 6.0.0 (#10635) Bumps [bullet](https://github.com/flyerhzm/bullet) from 5.9.0 to 6.0.0. - [Release notes](https://github.com/flyerhzm/bullet/releases) - [Changelog](https://github.com/flyerhzm/bullet/blob/master/CHANGELOG.md) - [Commits](https://github.com/flyerhzm/bullet/compare/5.9.0...6.0.0) Signed-off-by: dependabot[bot] <support@dependabot.com> --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index b908dc4945..8078e46a00 100644 --- a/Gemfile +++ b/Gemfile @@ -124,7 +124,7 @@ group :development do gem 'annotate', '~> 2.7' gem 'better_errors', '~> 2.5' gem 'binding_of_caller', '~> 0.7' - gem 'bullet', '~> 5.9' + gem 'bullet', '~> 6.0' gem 'letter_opener', '~> 1.7' gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' diff --git a/Gemfile.lock b/Gemfile.lock index d03ed2e607..188e99b136 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,7 +106,7 @@ GEM brakeman (4.5.0) browser (2.5.3) builder (3.2.3) - bullet (5.9.0) + bullet (6.0.0) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) bundler-audit (0.6.1) @@ -665,7 +665,7 @@ DEPENDENCIES bootsnap (~> 1.4) brakeman (~> 4.5) browser - bullet (~> 5.9) + bullet (~> 6.0) bundler-audit (~> 0.6) capistrano (~> 3.11) capistrano-rails (~> 1.4) From 825d5c79b76f0be59a6123d0b26c52b61ad87132 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2019 18:06:09 +0200 Subject: [PATCH 12/54] Bump annotate from 2.7.4 to 2.7.5 (#10651) Bumps [annotate](https://github.com/ctran/annotate_models) from 2.7.4 to 2.7.5. - [Release notes](https://github.com/ctran/annotate_models/releases) - [Changelog](https://github.com/ctran/annotate_models/blob/develop/CHANGELOG.rdoc) - [Commits](https://github.com/ctran/annotate_models/compare/v2.7.4...2.7.5) Signed-off-by: dependabot[bot] <support@dependabot.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 188e99b136..d1758cfaf0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -66,8 +66,8 @@ GEM public_suffix (>= 2.0.2, < 4.0) airbrussh (1.3.0) sshkit (>= 1.6.1, != 1.7.0) - annotate (2.7.4) - activerecord (>= 3.2, < 6.0) + annotate (2.7.5) + activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 13.0) arel (9.0.0) ast (2.4.0) From 699109b9545f489b7f4dcf44710c48a0fb27de8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2019 15:49:16 +0900 Subject: [PATCH 13/54] Bump rubocop from 0.68.0 to 0.68.1 (#10658) Bumps [rubocop](https://github.com/rubocop-hq/rubocop) from 0.68.0 to 0.68.1. - [Release notes](https://github.com/rubocop-hq/rubocop/releases) - [Changelog](https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop-hq/rubocop/compare/v0.68.0...v0.68.1) Signed-off-by: dependabot[bot] <support@dependabot.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d1758cfaf0..7e7526b3b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -527,7 +527,7 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.8.0) - rubocop (0.68.0) + rubocop (0.68.1) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) From 0db269f3dc1108630b987a45a68d7b8ec04a6ba6 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Wed, 1 May 2019 15:19:55 +0200 Subject: [PATCH 14/54] Minor fixes to the French translation (#10662) --- config/locales/fr.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a6c806de39..d588b239ff 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -260,10 +260,10 @@ fr: title: Nouveau blocage de domaine reject_media: Fichiers média rejetés reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions - reject_reports: Rapports de rejet - reject_reports_hint: Ignorez tous les rapports provenant de ce domaine. Sans objet pour les suspensions + reject_reports: Rejeter les signalements + reject_reports_hint: Ignorez tous les signalements provenant de ce domaine. Ne concerne pas les suspensions rejecting_media: rejet des fichiers multimédia - rejecting_reports: rejet de rapports + rejecting_reports: rejet des signalements severity: silence: silencié suspend: suspendu From c4f24333002a6b1cec06f53c2910700648654487 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Thu, 2 May 2019 00:10:19 +0200 Subject: [PATCH 15/54] Disallow robots from indexing /interact/ (#10666) This does not provide any new information and may just triple the number of crawled pages --- public/robots.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/public/robots.txt b/public/robots.txt index d93648beee..771bf2160b 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -2,3 +2,4 @@ User-agent: * Disallow: /media_proxy/ +Disallow: /interact/ From 21a73c52a7d0d0149e1058aeec155fe1c87aaeff Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Thu, 2 May 2019 04:30:12 +0200 Subject: [PATCH 16/54] Check that an invite link is valid before bypassing approval mode (#10657) * Check that an invite link is valid before bypassing approval mode Fixes #10656 * Add tests * Only consider valid invite links in registration controller * fixup --- .../auth/registrations_controller.rb | 3 +- app/models/user.rb | 2 +- .../auth/registrations_controller_spec.rb | 83 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 5c1ff769a8..83797cf1f7 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -91,7 +91,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_invite - @invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil + invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil + @invite = invite&.valid_for_use? ? invite : nil end def determine_layout diff --git a/app/models/user.rb b/app/models/user.rb index c42f6ad8d8..4320786518 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -114,7 +114,7 @@ class User < ApplicationRecord end def invited? - invite_id.present? + invite_id.present? && invite.valid_for_use? end def disable! diff --git a/spec/controllers/auth/registrations_controller_spec.rb b/spec/controllers/auth/registrations_controller_spec.rb index 1095df034e..a4337039e1 100644 --- a/spec/controllers/auth/registrations_controller_spec.rb +++ b/spec/controllers/auth/registrations_controller_spec.rb @@ -107,6 +107,89 @@ RSpec.describe Auth::RegistrationsController, type: :controller do end end + context 'approval-based registrations without invite' do + around do |example| + registrations_mode = Setting.registrations_mode + example.run + Setting.registrations_mode = registrations_mode + end + + subject do + Setting.registrations_mode = 'approved' + request.headers["Accept-Language"] = accept_language + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678' } } + end + + it 'redirects to login page' do + subject + expect(response).to redirect_to new_user_session_path + end + + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + expect(user.approved).to eq(false) + end + end + + context 'approval-based registrations with expired invite' do + around do |example| + registrations_mode = Setting.registrations_mode + example.run + Setting.registrations_mode = registrations_mode + end + + subject do + Setting.registrations_mode = 'approved' + request.headers["Accept-Language"] = accept_language + invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.ago) + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } } + end + + it 'redirects to login page' do + subject + expect(response).to redirect_to new_user_session_path + end + + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + expect(user.approved).to eq(false) + end + end + + context 'approval-based registrations with valid invite' do + around do |example| + registrations_mode = Setting.registrations_mode + example.run + Setting.registrations_mode = registrations_mode + end + + subject do + Setting.registrations_mode = 'approved' + request.headers["Accept-Language"] = accept_language + invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now) + post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code } } + end + + it 'redirects to login page' do + subject + expect(response).to redirect_to new_user_session_path + end + + it 'creates user' do + subject + user = User.find_by(email: 'test@example.com') + expect(user).to_not be_nil + expect(user.locale).to eq(accept_language) + expect(user.approved).to eq(true) + end + end + it 'does nothing if user already exists' do Fabricate(:user, account: Fabricate(:account, username: 'test')) subject From 3f143606faa6181ff2745b6bd29ac8ea075088bf Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Thu, 2 May 2019 08:34:32 +0200 Subject: [PATCH 17/54] Change account gallery in web UI (#10667) - 3 items per row instead of 2 - Use blurhash for previews - Animate/hover-to-play GIFs and videos - Open media modal instead of opening status - Allow opening status instead with ctrl+click and open in new tab --- .../mastodon/components/media_gallery.js | 2 +- .../account_gallery/components/media_item.js | 150 ++++++++++++++---- .../features/account_gallery/index.js | 73 +++++---- .../styles/mastodon/components.scss | 60 ++----- 4 files changed, 170 insertions(+), 115 deletions(-) diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index f548296d06..7c1d3c3e98 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -157,7 +157,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} > + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index 80ac9d9ecb..8d462996ed 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -1,62 +1,142 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Permalink from '../../../components/permalink'; -import { displayMedia } from '../../../initial_state'; -import Icon from 'mastodon/components/icon'; +import { autoPlayGif, displayMedia } from 'mastodon/initial_state'; +import classNames from 'classnames'; +import { decode } from 'blurhash'; +import { isIOS } from 'mastodon/is_mobile'; export default class MediaItem extends ImmutablePureComponent { static propTypes = { - media: ImmutablePropTypes.map.isRequired, + attachment: ImmutablePropTypes.map.isRequired, + displayWidth: PropTypes.number.isRequired, + onOpenMedia: PropTypes.func.isRequired, }; state = { - visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + loaded: false, }; - handleClick = () => { - if (!this.state.visible) { - this.setState({ visible: true }); - return true; + componentDidMount () { + if (this.props.attachment.get('blurhash')) { + this._decode(); } + } - return false; + 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 }); + } + + handleMouseEnter = e => { + if (this.hoverToPlay()) { + e.target.play(); + } + } + + handleMouseLeave = e => { + if (this.hoverToPlay()) { + e.target.pause(); + e.target.currentTime = 0; + } + } + + hoverToPlay () { + return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1; + } + + handleClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + + if (this.state.visible) { + this.props.onOpenMedia(this.props.attachment); + } else { + this.setState({ visible: true }); + } + } } render () { - const { media } = this.props; - const { visible } = this.state; - const status = media.get('status'); - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const style = {}; + const { attachment, displayWidth } = this.props; + const { visible, loaded } = this.state; - let label, icon; + const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; + const height = width; + const status = attachment.get('status'); - if (media.get('type') === 'gifv') { - label = <span className='media-gallery__gifv__label'>GIF</span>; - } + let thumbnail = ''; - if (visible) { - style.backgroundImage = `url(${media.get('preview_url')})`; - style.backgroundPosition = `${x}% ${y}%`; - } else { - icon = ( - <span className='account-gallery__item__icons'> - <Icon id='eye-slash' /> - </span> + if (attachment.get('type') === 'unknown') { + // Skip + } else if (attachment.get('type') === 'image') { + const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; + const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; + const x = ((focusX / 2) + .5) * 100; + const y = ((focusY / -2) + .5) * 100; + + thumbnail = ( + <img + src={attachment.get('preview_url')} + alt={attachment.get('description')} + title={attachment.get('description')} + style={{ objectPosition: `${x}% ${y}%` }} + onLoad={this.handleImageLoad} + /> + ); + } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { + const autoPlay = !isIOS() && autoPlayGif; + + thumbnail = ( + <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> + <video + className='media-gallery__item-gifv-thumbnail' + aria-label={attachment.get('description')} + title={attachment.get('description')} + role='application' + src={attachment.get('url')} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + autoPlay={autoPlay} + loop + muted + /> + + <span className='media-gallery__gifv__label'>GIF</span> + </div> ); } return ( - <div className='account-gallery__item'> - <Permalink to={`/statuses/${status.get('id')}`} href={status.get('url')} style={style} onInterceptClick={this.handleClick}> - {icon} - {label} - </Permalink> + <div className='account-gallery__item' style={{ width, height }}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' style={{ cursor: 'pointer' }} onClick={this.handleClick}> + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> + {visible && thumbnail} + </a> </div> ); } diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 73be58d6a3..8766473fe2 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -2,24 +2,25 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { fetchAccount } from '../../actions/accounts'; +import { fetchAccount } from 'mastodon/actions/accounts'; import { expandAccountMediaTimeline } from '../../actions/timelines'; -import LoadingIndicator from '../../components/loading_indicator'; +import LoadingIndicator from 'mastodon/components/loading_indicator'; import Column from '../ui/components/column'; -import ColumnBackButton from '../../components/column_back_button'; +import ColumnBackButton from 'mastodon/components/column_back_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getAccountGallery } from '../../selectors'; +import { getAccountGallery } from 'mastodon/selectors'; import MediaItem from './components/media_item'; import HeaderContainer from '../account_timeline/containers/header_container'; import { ScrollContainer } from 'react-router-scroll-4'; -import LoadMore from '../../components/load_more'; +import LoadMore from 'mastodon/components/load_more'; import MissingIndicator from 'mastodon/components/missing_indicator'; +import { openModal } from 'mastodon/actions/modal'; const mapStateToProps = (state, props) => ({ isAccount: !!state.getIn(['accounts', props.params.accountId]), - medias: getAccountGallery(state, props.params.accountId), + attachments: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), + hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), }); class LoadMoreMedia extends ImmutablePureComponent { @@ -51,12 +52,16 @@ class AccountGallery extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, - medias: ImmutablePropTypes.list.isRequired, + attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, }; + state = { + width: 323, + }; + componentDidMount () { this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); @@ -71,11 +76,11 @@ class AccountGallery extends ImmutablePureComponent { handleScrollToBottom = () => { if (this.props.hasMore) { - this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined); + this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined); } } - handleScroll = (e) => { + handleScroll = e => { const { scrollTop, scrollHeight, clientHeight } = e.target; const offset = scrollHeight - scrollTop - clientHeight; @@ -88,13 +93,31 @@ class AccountGallery extends ImmutablePureComponent { this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); }; - handleLoadOlder = (e) => { + handleLoadOlder = e => { e.preventDefault(); this.handleScrollToBottom(); } + handleOpenMedia = attachment => { + if (attachment.get('type') === 'video') { + this.props.dispatch(openModal('VIDEO', { media: attachment })); + } else { + const media = attachment.getIn(['status', 'media_attachments']); + const index = media.findIndex(x => x.get('id') === attachment.get('id')); + + this.props.dispatch(openModal('MEDIA', { media, index })); + } + } + + handleRef = c => { + if (c) { + this.setState({ width: c.offsetWidth }); + } + } + render () { - const { medias, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props; + const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props; + const { width } = this.state; if (!isAccount) { return ( @@ -104,9 +127,7 @@ class AccountGallery extends ImmutablePureComponent { ); } - let loadOlder = null; - - if (!medias && isLoading) { + if (!attachments && isLoading) { return ( <Column> <LoadingIndicator /> @@ -114,7 +135,9 @@ class AccountGallery extends ImmutablePureComponent { ); } - if (hasMore && !(isLoading && medias.size === 0)) { + let loadOlder = null; + + if (hasMore && !(isLoading && attachments.size === 0)) { loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />; } @@ -126,23 +149,17 @@ class AccountGallery extends ImmutablePureComponent { <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <HeaderContainer accountId={this.props.params.accountId} /> - <div role='feed' className='account-gallery__container'> - {medias.map((media, index) => media === null ? ( - <LoadMoreMedia - key={'more:' + medias.getIn(index + 1, 'id')} - maxId={index > 0 ? medias.getIn(index - 1, 'id') : null} - onLoadMore={this.handleLoadMore} - /> + <div role='feed' className='account-gallery__container' ref={this.handleRef}> + {attachments.map((attachment, index) => attachment === null ? ( + <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> ) : ( - <MediaItem - key={media.get('id')} - media={media} - /> + <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} /> ))} + {loadOlder} </div> - {isLoading && medias.size === 0 && ( + {isLoading && attachments.size === 0 && ( <div className='scrollable__append'> <LoadingIndicator /> </div> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 48970f8bd7..0e942d234e 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4233,6 +4233,7 @@ a.status-card.compact:hover { pointer-events: none; opacity: 0.9; transition: opacity 0.1s ease; + line-height: 18px; } .media-gallery__gifv { @@ -4762,62 +4763,19 @@ a.status-card.compact:hover { .account-gallery__container { display: flex; - justify-content: center; flex-wrap: wrap; - padding: 2px; + justify-content: center; + padding: 4px 2px; } .account-gallery__item { - flex-grow: 1; - width: 50%; - overflow: hidden; + border: none; + box-sizing: border-box; + display: block; position: relative; - - &::before { - content: ""; - display: block; - padding-top: 100%; - } - - a { - display: block; - width: calc(100% - 4px); - height: calc(100% - 4px); - margin: 2px; - top: 0; - left: 0; - background-color: $base-overlay-background; - background-size: cover; - background-position: center; - position: absolute; - color: $darker-text-color; - text-decoration: none; - border-radius: 4px; - - &:hover, - &:active, - &:focus { - outline: 0; - color: $secondary-text-color; - - &::before { - content: ""; - display: block; - width: 100%; - height: 100%; - background: rgba($base-overlay-background, 0.3); - border-radius: 4px; - } - } - } - - &__icons { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 24px; - } + border-radius: 4px; + overflow: hidden; + margin: 2px; } .notification__filter-bar, From 967e419f8fa87af74f4bb530d7493c1dde02fca8 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 04:02:55 +0200 Subject: [PATCH 18/54] Fix alignment of items in the account gallery in web UI and load more per page (#10674) --- app/javascript/mastodon/actions/timelines.js | 2 +- app/javascript/styles/mastodon/components.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index d92385e951..06c21b96b7 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -96,7 +96,7 @@ export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0e942d234e..ebf46074b1 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4764,7 +4764,6 @@ a.status-card.compact:hover { .account-gallery__container { display: flex; flex-wrap: wrap; - justify-content: center; padding: 4px 2px; } From 05ef3462ba0af7b147a7cfa8de2735e99dc59ac5 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 04:34:55 +0200 Subject: [PATCH 19/54] Make the "mark media as sensitive" button more obvious in web UI (#10673) * Make the "mark media as sensitive" button more obvious in web UI * Use eye-slash icon instead of eye icon to mean "hide" --- .../mastodon/components/media_gallery.js | 2 +- .../compose/components/compose_form.js | 2 - .../compose/components/upload_form.js | 3 ++ .../containers/sensitive_button_container.js | 40 +++++-------------- .../mastodon/features/video/index.js | 2 +- .../styles/mastodon/components.scss | 5 +++ 6 files changed, 19 insertions(+), 35 deletions(-) diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 7c1d3c3e98..abd17647eb 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -314,7 +314,7 @@ class MediaGallery extends React.PureComponent { } if (visible) { - spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />; + spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />; } else { spoilerButton = ( <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> diff --git a/app/javascript/mastodon/features/compose/components/compose_form.js b/app/javascript/mastodon/features/compose/components/compose_form.js index ddb610a89c..2b9da20d7f 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.js +++ b/app/javascript/mastodon/features/compose/components/compose_form.js @@ -10,7 +10,6 @@ import UploadButtonContainer from '../containers/upload_button_container'; import { defineMessages, injectIntl } from 'react-intl'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import SensitiveButtonContainer from '../containers/sensitive_button_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import PollFormContainer from '../containers/poll_form_container'; import UploadFormContainer from '../containers/upload_form_container'; @@ -214,7 +213,6 @@ class ComposeForm extends ImmutablePureComponent { <UploadButtonContainer /> <PollButtonContainer /> <PrivacyDropdownContainer /> - <SensitiveButtonContainer /> <SpoilerButtonContainer /> </div> <div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js index b7f1122053..9ff2aa0fa6 100644 --- a/app/javascript/mastodon/features/compose/components/upload_form.js +++ b/app/javascript/mastodon/features/compose/components/upload_form.js @@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; +import SensitiveButtonContainer from '../containers/sensitive_button_container'; export default class UploadForm extends ImmutablePureComponent { @@ -22,6 +23,8 @@ export default class UploadForm extends ImmutablePureComponent { <UploadContainer id={id} key={id} /> ))} </div> + + {!mediaIds.isEmpty() && <SensitiveButtonContainer />} </div> ); } diff --git a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js index 43de8f213e..50612b086a 100644 --- a/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js +++ b/app/javascript/mastodon/features/compose/containers/sensitive_button_container.js @@ -2,11 +2,9 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import IconButton from '../../../components/icon_button'; -import { changeComposeSensitivity } from '../../../actions/compose'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { injectIntl, defineMessages } from 'react-intl'; +import { changeComposeSensitivity } from 'mastodon/actions/compose'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Icon from 'mastodon/components/icon'; const messages = defineMessages({ marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, @@ -14,7 +12,6 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - visible: state.getIn(['compose', 'media_attachments']).size > 0, active: state.getIn(['compose', 'sensitive']), disabled: state.getIn(['compose', 'spoiler']), }); @@ -30,7 +27,6 @@ const mapDispatchToProps = dispatch => ({ class SensitiveButton extends React.PureComponent { static propTypes = { - visible: PropTypes.bool, active: PropTypes.bool, disabled: PropTypes.bool, onClick: PropTypes.func.isRequired, @@ -38,32 +34,14 @@ class SensitiveButton extends React.PureComponent { }; render () { - const { visible, active, disabled, onClick, intl } = this.props; + const { active, disabled, onClick, intl } = this.props; return ( - <Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}> - {({ scale }) => { - const icon = active ? 'eye-slash' : 'eye'; - const className = classNames('compose-form__sensitive-button', { - 'compose-form__sensitive-button--visible': visible, - }); - return ( - <div className={className} style={{ transform: `scale(${scale})` }}> - <IconButton - className='compose-form__sensitive-button__icon' - title={intl.formatMessage(active ? messages.marked : messages.unmarked)} - icon={icon} - onClick={onClick} - size={18} - active={active} - disabled={disabled} - style={{ lineHeight: null, height: null }} - inverted - /> - </div> - ); - }} - </Motion> + <div className='compose-form__sensitive-button'> + <button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <Icon id='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + </button> + </div> ); } diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 7b6113e6a4..8ae26eba8f 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -472,7 +472,7 @@ class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye' fixedWidth /></button>} + {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>} {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>} {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>} <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ebf46074b1..8aebe3432b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -264,6 +264,11 @@ .compose-form { padding: 10px; + &__sensitive-button { + padding: 10px; + padding-top: 0; + } + .compose-form__warning { color: $inverted-text-color; margin-bottom: 10px; From 5121d9c12f39e95eaef630dd6c98b736cb76c4c0 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Fri, 3 May 2019 06:20:36 +0200 Subject: [PATCH 20/54] When selecting a toot via keyboard, ensure it is scrolled into view (#10593) --- .../mastodon/components/status_list.js | 14 ++++++++---- .../components/conversations_list.js | 14 ++++++++---- .../mastodon/features/notifications/index.js | 14 ++++++++---- .../mastodon/features/status/index.js | 22 ++++++++++++------- app/javascript/mastodon/features/ui/index.js | 9 ++++++-- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index e417f9a2b9..745e6422d3 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -46,22 +46,28 @@ export default class StatusList extends ImmutablePureComponent { handleMoveUp = (id, featured) => { const elementIndex = this.getCurrentStatusIndex(id, featured) - 1; - this._selectChild(elementIndex); + this._selectChild(elementIndex, true); } handleMoveDown = (id, featured) => { const elementIndex = this.getCurrentStatusIndex(id, featured) + 1; - this._selectChild(elementIndex); + this._selectChild(elementIndex, false); } handleLoadOlder = debounce(() => { this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined); }, 300, { leading: true }) - _selectChild (index) { - const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + _selectChild (index, align_top) { + const container = this.node.node; + const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); if (element) { + if (align_top && container.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); + } element.focus(); } } diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js index 635c03c1d1..8867bbd738 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversations_list.js @@ -20,18 +20,24 @@ export default class ConversationsList extends ImmutablePureComponent { handleMoveUp = id => { const elementIndex = this.getCurrentIndex(id) - 1; - this._selectChild(elementIndex); + this._selectChild(elementIndex, true); } handleMoveDown = id => { const elementIndex = this.getCurrentIndex(id) + 1; - this._selectChild(elementIndex); + this._selectChild(elementIndex, false); } - _selectChild (index) { - const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + _selectChild (index, align_top) { + const container = this.node.node; + const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); if (element) { + if (align_top && container.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); + } element.focus(); } } diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 9430b20505..006c456575 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -113,18 +113,24 @@ class Notifications extends React.PureComponent { handleMoveUp = id => { const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1; - this._selectChild(elementIndex); + this._selectChild(elementIndex, true); } handleMoveDown = id => { const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1; - this._selectChild(elementIndex); + this._selectChild(elementIndex, false); } - _selectChild (index) { - const element = this.column.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + _selectChild (index, align_top) { + const container = this.column.node; + const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`); if (element) { + if (align_top && container.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); + } element.focus(); } } diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 567af6be9f..6279bb468e 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -316,15 +316,15 @@ class Status extends ImmutablePureComponent { const { status, ancestorsIds, descendantsIds } = this.props; if (id === status.get('id')) { - this._selectChild(ancestorsIds.size - 1); + this._selectChild(ancestorsIds.size - 1, true); } else { let index = ancestorsIds.indexOf(id); if (index === -1) { index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index); + this._selectChild(ancestorsIds.size + index, true); } else { - this._selectChild(index - 1); + this._selectChild(index - 1, true); } } } @@ -333,23 +333,29 @@ class Status extends ImmutablePureComponent { const { status, ancestorsIds, descendantsIds } = this.props; if (id === status.get('id')) { - this._selectChild(ancestorsIds.size + 1); + this._selectChild(ancestorsIds.size + 1, false); } else { let index = ancestorsIds.indexOf(id); if (index === -1) { index = descendantsIds.indexOf(id); - this._selectChild(ancestorsIds.size + index + 2); + this._selectChild(ancestorsIds.size + index + 2, false); } else { - this._selectChild(index + 1); + this._selectChild(index + 1, false); } } } - _selectChild (index) { - const element = this.node.querySelectorAll('.focusable')[index]; + _selectChild (index, align_top) { + const container = this.node; + const element = container.querySelectorAll('.focusable')[index]; if (element) { + if (align_top && container.scrollTop > element.offsetTop) { + element.scrollIntoView(true); + } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { + element.scrollIntoView(false); + } element.focus(); } } diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 93e45678f2..c14eba9925 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -367,11 +367,16 @@ class UI extends React.PureComponent { handleHotkeyFocusColumn = e => { const index = (e.key * 1) + 1; // First child is drawer, skip that const column = this.node.querySelector(`.column:nth-child(${index})`); + if (!column) return; + const container = column.querySelector('.scrollable'); - if (column) { - const status = column.querySelector('.focusable'); + if (container) { + const status = container.querySelector('.focusable'); if (status) { + if (container.scrollTop > status.offsetTop) { + status.scrollIntoView(true); + } status.focus(); } } From 153b4ffc78e07f59793b2ae7bfcd7a2324a45621 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2019 15:01:38 +0900 Subject: [PATCH 21/54] Bump fabrication from 2.20.1 to 2.20.2 (#10677) Bumps [fabrication](https://github.com/paulelliott/fabrication) from 2.20.1 to 2.20.2. - [Release notes](https://github.com/paulelliott/fabrication/releases) - [Changelog](https://github.com/paulelliott/fabrication/blob/master/Changelog.markdown) - [Commits](https://github.com/paulelliott/fabrication/compare/2.20.1...2.20.2) Signed-off-by: dependabot[bot] <support@dependabot.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7e7526b3b8..61dd53c5e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -207,7 +207,7 @@ GEM et-orbi (1.1.6) tzinfo excon (0.62.0) - fabrication (2.20.1) + fabrication (2.20.2) faker (1.9.3) i18n (>= 0.7) faraday (0.15.0) From 61e28b0ccc04f7a0c72cb663962eb152ab63a998 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2019 17:29:53 +0900 Subject: [PATCH 22/54] Bump scss_lint from 0.57.1 to 0.58.0 (#10678) Bumps [scss_lint](https://github.com/sds/scss-lint) from 0.57.1 to 0.58.0. - [Release notes](https://github.com/sds/scss-lint/releases) - [Changelog](https://github.com/sds/scss-lint/blob/master/CHANGELOG.md) - [Commits](https://github.com/sds/scss-lint/compare/v0.57.1...v0.58.0) Signed-off-by: dependabot[bot] <support@dependabot.com> --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 8078e46a00..67df76be69 100644 --- a/Gemfile +++ b/Gemfile @@ -131,7 +131,7 @@ group :development do gem 'rubocop', '~> 0.68', require: false gem 'brakeman', '~> 4.5', require: false gem 'bundler-audit', '~> 0.6', require: false - gem 'scss_lint', '~> 0.57', require: false + gem 'scss_lint', '~> 0.58', require: false gem 'capistrano', '~> 3.11' gem 'capistrano-rails', '~> 1.4' diff --git a/Gemfile.lock b/Gemfile.lock index 61dd53c5e4..09b0de6143 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -471,8 +471,8 @@ GEM rainbow (3.0.0) rake (12.3.2) rb-fsevent (0.10.3) - rb-inotify (0.9.10) - ffi (>= 0.5.0, < 2) + rb-inotify (0.10.0) + ffi (~> 1.0) rdf (3.0.9) hamster (~> 3.0) link_header (~> 0.0, >= 0.0.8) @@ -544,12 +544,12 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.8.0) nokogumbo (~> 2.0) - sass (3.6.0) + sass (3.7.4) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - scss_lint (0.57.1) + scss_lint (0.58.0) rake (>= 0.9, < 13) sass (~> 3.5, >= 3.5.5) sidekiq (5.2.7) @@ -750,7 +750,7 @@ DEPENDENCIES rspec-sidekiq (~> 3.0) rubocop (~> 0.68) sanitize (~> 5.0) - scss_lint (~> 0.57) + scss_lint (~> 0.58) sidekiq (~> 5.2) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) From ecbea2e3c6e49387b1eaefbbebd2013867414ca2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <dependabot[bot]@users.noreply.github.com> Date: Fri, 3 May 2019 16:16:11 +0200 Subject: [PATCH 23/54] Bump rack-attack from 5.4.2 to 6.0.0 (#10599) * Bump rack-attack from 5.4.2 to 6.0.0 Bumps [rack-attack](https://github.com/kickstarter/rack-attack) from 5.4.2 to 6.0.0. - [Release notes](https://github.com/kickstarter/rack-attack/releases) - [Changelog](https://github.com/kickstarter/rack-attack/blob/master/CHANGELOG.md) - [Commits](https://github.com/kickstarter/rack-attack/compare/v5.4.2...v6.0.0) Signed-off-by: dependabot[bot] <support@dependabot.com> * fix payload[:request] --- Gemfile | 2 +- Gemfile.lock | 4 ++-- config/initializers/rack_attack_logging.rb | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 67df76be69..db00c24fb9 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,7 @@ gem 'ox', '~> 2.10' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 2.0' gem 'premailer-rails' -gem 'rack-attack', '~> 5.4' +gem 'rack-attack', '~> 6.0' gem 'rack-cors', '~> 1.0', require: 'rack/cors' gem 'rails-i18n', '~> 5.1' gem 'rails-settings-cached', '~> 0.6' diff --git a/Gemfile.lock b/Gemfile.lock index 09b0de6143..7ab907f6d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -426,7 +426,7 @@ GEM activesupport (>= 3.0.0) raabro (1.1.6) rack (2.0.7) - rack-attack (5.4.2) + rack-attack (6.0.0) rack (>= 1.0, < 3) rack-cors (1.0.3) rack-protection (2.0.5) @@ -735,7 +735,7 @@ DEPENDENCIES pry-rails (~> 0.3) puma (~> 3.12) pundit (~> 2.0) - rack-attack (~> 5.4) + rack-attack (~> 6.0) rack-cors (~> 1.0) rails (~> 5.2.3) rails-controller-testing (~> 1.0) diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb index 2ddbfb99ce..c30bd8a643 100644 --- a/config/initializers/rack_attack_logging.rb +++ b/config/initializers/rack_attack_logging.rb @@ -1,4 +1,6 @@ -ActiveSupport::Notifications.subscribe('rack.attack') do |_name, _start, _finish, _request_id, req| +ActiveSupport::Notifications.subscribe(/rack_attack/) do |_name, _start, _finish, _request_id, payload| + req = payload[:request] + next unless [:throttle, :blacklist].include? req.env['rack.attack.match_type'] Rails.logger.info("Rate limit hit (#{req.env['rack.attack.match_type']}): #{req.ip} #{req.request_method} #{req.fullpath}") end From eb63217210b0ab85ff1fcca9506d5e7931382a56 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 16:16:30 +0200 Subject: [PATCH 24/54] Add button to view context to media modal (#10676) * Add "view context" button to media modal when opened from gallery * Add "view context" button to video modal Allow closing the video modal by navigating back in the browser, just like the media modal --- .../features/account_gallery/index.js | 4 +- .../features/ui/components/media_modal.js | 31 ++++++++++--- .../features/ui/components/video_modal.js | 44 ++++++++++++++++++- .../mastodon/features/video/index.js | 10 +++-- .../styles/mastodon/components.scss | 42 ++++++++++++++++++ 5 files changed, 119 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index 8766473fe2..5d6a53e18e 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -100,12 +100,12 @@ class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { if (attachment.get('type') === 'video') { - this.props.dispatch(openModal('VIDEO', { media: attachment })); + this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - this.props.dispatch(openModal('MEDIA', { media, index })); + this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') })); } } diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 848cb20b3c..da2ac5f26d 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -2,11 +2,11 @@ import React from 'react'; import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import Video from '../../video'; -import ExtendedVideoPlayer from '../../../components/extended_video_player'; +import Video from 'mastodon/features/video'; +import ExtendedVideoPlayer from 'mastodon/components/extended_video_player'; import classNames from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; -import IconButton from '../../../components/icon_button'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import IconButton from 'mastodon/components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; import Icon from 'mastodon/components/icon'; @@ -24,6 +24,7 @@ class MediaModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.list.isRequired, + status: ImmutablePropTypes.map, index: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -72,9 +73,12 @@ class MediaModal extends ImmutablePureComponent { componentDidMount () { window.addEventListener('keydown', this.handleKeyDown, false); + if (this.context.router) { const history = this.context.router.history; + history.push(history.location.pathname, previewState); + this.unlistenHistory = history.listen(() => { this.props.onClose(); }); @@ -83,6 +87,7 @@ class MediaModal extends ImmutablePureComponent { componentWillUnmount () { window.removeEventListener('keydown', this.handleKeyDown); + if (this.context.router) { this.unlistenHistory(); @@ -102,8 +107,15 @@ class MediaModal extends ImmutablePureComponent { })); }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, intl, onClose } = this.props; + const { media, status, intl, onClose } = this.props; const { navigationHidden } = this.state; const index = this.getIndex(); @@ -207,10 +219,19 @@ class MediaModal extends ImmutablePureComponent { {content} </ReactSwipeableViews> </div> + <div className={navigationClassName}> <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} /> + {leftNav} {rightNav} + + {status && ( + <div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}> + <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> + </div> + )} + <ul className='media-modal__pagination'> {pagination} </ul> diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js index 52457a630b..213d31316c 100644 --- a/app/javascript/mastodon/features/ui/components/video_modal.js +++ b/app/javascript/mastodon/features/ui/components/video_modal.js @@ -1,19 +1,58 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import Video from '../../video'; +import Video from 'mastodon/features/video'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; + +export const previewState = 'previewVideoModal'; export default class VideoModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map, time: PropTypes.number, onClose: PropTypes.func.isRequired, }; + static contextTypes = { + router: PropTypes.object, + }; + + componentDidMount () { + if (this.context.router) { + const history = this.context.router.history; + + history.push(history.location.pathname, previewState); + + this.unlistenHistory = history.listen(() => { + this.props.onClose(); + }); + } + } + + componentWillUnmount () { + if (this.context.router) { + this.unlistenHistory(); + + if (this.context.router.history.location.state === previewState) { + this.context.router.history.goBack(); + } + } + } + + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, time, onClose } = this.props; + const { media, status, time, onClose } = this.props; + + const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>; return ( <div className='modal-root__modal video-modal'> @@ -24,6 +63,7 @@ export default class VideoModal extends ImmutablePureComponent { src={media.get('url')} startTime={time} onCloseVideo={onClose} + link={link} detailed alt={media.get('description')} /> diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 8ae26eba8f..00a63a3d9f 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -104,6 +104,7 @@ class Video extends React.PureComponent { cacheWidth: PropTypes.func, intl: PropTypes.object.isRequired, blurhash: PropTypes.string, + link: PropTypes.node, }; state = { @@ -361,7 +362,7 @@ class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; @@ -453,6 +454,7 @@ class Video extends React.PureComponent { <div className='video-player__buttons left'> <button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> + <div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> <div className='video-player__volume__current' style={{ width: `${volumeWidth}px` }} /> <span @@ -462,13 +464,15 @@ class Video extends React.PureComponent { /> </div> - {(detailed || fullscreen) && + {(detailed || fullscreen) && ( <span> <span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-sep'>/</span> <span className='video-player__time-total'>{formatTime(duration)}</span> </span> - } + )} + + {link && <span className='video-player__link'>{link}</span>} </div> <div className='video-player__buttons right'> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8aebe3432b..c1466aa88e 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3766,6 +3766,31 @@ a.status-card.compact:hover { pointer-events: none; } +.media-modal__meta { + text-align: center; + position: absolute; + left: 0; + bottom: 20px; + width: 100%; + pointer-events: none; + + &--shifted { + bottom: 62px; + } + + a { + text-decoration: none; + font-weight: 500; + color: $ui-secondary-color; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } +} + .media-modal__page-dot { display: inline-block; } @@ -4676,6 +4701,23 @@ a.status-card.compact:hover { } } + &__link { + padding: 2px 10px; + + a { + text-decoration: none; + font-size: 14px; + font-weight: 500; + color: $white; + + &:hover, + &:active, + &:focus { + text-decoration: underline; + } + } + } + &__seek { cursor: pointer; height: 24px; From 011b032300657ccca4a42866749afc6ec2588ecc Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Fri, 3 May 2019 20:36:36 +0200 Subject: [PATCH 25/54] Provide a link to existing domain block when trying to block an already-blocked domain (#10663) * When trying to block an already-blocked domain, provide a link to the block * Fix styling for links in flash messages * Allow blocks to be upgraded but not downgraded --- .../admin/domain_blocks_controller.rb | 22 ++++++++++--- app/javascript/styles/mastodon/forms.scss | 11 +++++++ app/models/domain_block.rb | 7 +++++ config/locales/en.yml | 1 + .../admin/domain_blocks_controller_spec.rb | 13 +++++++- spec/models/domain_block_spec.rb | 31 +++++++++++++++++++ 6 files changed, 79 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 5f307ddeec..dd3f833890 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -13,13 +13,25 @@ module Admin authorize :domain_block, :create? @domain_block = DomainBlock.new(resource_params) + existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - log_action :create, @domain_block - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') - else + if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) + @domain_block.save + flash[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety + @domain_block.errors[:domain].clear render :new + else + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.update(resource_params) + end + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :new + end end end diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index 91888d3059..2b8d7a6825 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -533,6 +533,17 @@ code { color: $error-value-color; } + a { + display: inline-block; + color: $darker-text-color; + text-decoration: none; + + &:hover { + color: $primary-text-color; + text-decoration: underline; + } + } + p { margin-bottom: 15px; } diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 069cda3673..0b12617c6c 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -29,4 +29,11 @@ class DomainBlock < ApplicationRecord def self.blocked?(domain) where(domain: domain, severity: :suspend).exists? end + + def stricter_than?(other_block) + return true if suspend? + return false if other_block.suspend? && (silence? || noop?) + return false if other_block.silence? && noop? + (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports) + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 405b0eda5a..6d59411a58 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -269,6 +269,7 @@ en: created_msg: Domain block is now being processed destroyed_msg: Domain block has been undone domain: Domain + existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to <a href="%{unblock_url}">unblock it</a> first. new: create: Create block hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index 129bf88837..2a8675c21a 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do end it 'renders new when failed to save' do - Fabricate(:domain_block, domain: 'example.com') + Fabricate(:domain_block, domain: 'example.com', severity: 'suspend') allow(DomainBlockWorker).to receive(:perform_async).and_return(true) post :create, params: { domain_block: { domain: 'example.com', severity: 'silence' } } @@ -45,6 +45,17 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do expect(DomainBlockWorker).not_to have_received(:perform_async) expect(response).to render_template :new end + + it 'allows upgrading a block' do + Fabricate(:domain_block, domain: 'example.com', severity: 'silence') + allow(DomainBlockWorker).to receive(:perform_async).and_return(true) + + post :create, params: { domain_block: { domain: 'example.com', severity: 'silence', reject_media: true, reject_reports: true } } + + expect(DomainBlockWorker).to have_received(:perform_async) + expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') + expect(response).to redirect_to(admin_instances_path(limited: '1')) + end end describe 'DELETE #destroy' do diff --git a/spec/models/domain_block_spec.rb b/spec/models/domain_block_spec.rb index 89cadccfec..0035fd0ffa 100644 --- a/spec/models/domain_block_spec.rb +++ b/spec/models/domain_block_spec.rb @@ -36,4 +36,35 @@ RSpec.describe DomainBlock, type: :model do expect(DomainBlock.blocked?('domain')).to eq false end end + + describe 'stricter_than?' do + it 'returns true if the new block has suspend severity while the old has lower severity' do + suspend = DomainBlock.new(domain: 'domain', severity: :suspend) + silence = DomainBlock.new(domain: 'domain', severity: :silence) + noop = DomainBlock.new(domain: 'domain', severity: :noop) + expect(suspend.stricter_than?(silence)).to be true + expect(suspend.stricter_than?(noop)).to be true + end + + it 'returns false if the new block has lower severity than the old one' do + suspend = DomainBlock.new(domain: 'domain', severity: :suspend) + silence = DomainBlock.new(domain: 'domain', severity: :silence) + noop = DomainBlock.new(domain: 'domain', severity: :noop) + expect(silence.stricter_than?(suspend)).to be false + expect(noop.stricter_than?(suspend)).to be false + expect(noop.stricter_than?(silence)).to be false + end + + it 'returns false if the new block does is less strict regarding reports' do + older = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: true) + newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_reports: false) + expect(newer.stricter_than?(older)).to be false + end + + it 'returns false if the new block does is less strict regarding media' do + older = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: true) + newer = DomainBlock.new(domain: 'domain', severity: :silence, reject_media: false) + expect(newer.stricter_than?(older)).to be false + end + end end From 91634947f88fb3004b5e853598f02fbe39a55768 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Fri, 3 May 2019 20:39:19 +0200 Subject: [PATCH 26/54] Explicitly disable storage of REST API results (#10655) Fixes #10652 --- app/controllers/api/base_controller.rb | 6 ++++++ app/controllers/api/v1/custom_emojis_controller.rb | 2 ++ app/controllers/api/v1/instances/activity_controller.rb | 1 + app/controllers/api/v1/instances/peers_controller.rb | 1 + app/controllers/api/v1/instances_controller.rb | 1 + 5 files changed, 11 insertions(+) diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 3a92ee4e4d..eca558f421 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -9,6 +9,8 @@ class Api::BaseController < ApplicationController skip_before_action :store_current_location skip_before_action :check_user_permissions + before_action :set_cache_headers + protect_from_forgery with: :null_session rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| @@ -88,4 +90,8 @@ class Api::BaseController < ApplicationController def authorize_if_got_token!(*scopes) doorkeeper_authorize!(*scopes) if doorkeeper_token end + + def set_cache_headers + response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate' + end end diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 7bac27da4b..1bb19a09d3 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -3,6 +3,8 @@ class Api::V1::CustomEmojisController < Api::BaseController respond_to :json + skip_before_action :set_cache_headers + def index render_cached_json('api:v1:custom_emojis', expires_in: 1.minute) do ActiveModelSerializers::SerializableResource.new(CustomEmoji.local.where(disabled: false), each_serializer: REST::CustomEmojiSerializer) diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index e14e0aee83..09edfe365b 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! + skip_before_action :set_cache_headers respond_to :json diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 2070c487df..a8891d126b 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! + skip_before_action :set_cache_headers respond_to :json diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 5686e8d7c3..8c83a18014 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -2,6 +2,7 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json + skip_before_action :set_cache_headers def show render_cached_json('api:v1:instances', expires_in: 5.minutes) do From 63b1388fefff9414c2d0f9883f2d33f7c73284c6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 20:44:20 +0200 Subject: [PATCH 27/54] Change font weight of sensitive button to 500 (#10682) --- app/javascript/styles/mastodon/components.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c1466aa88e..2326dee38b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -267,6 +267,11 @@ &__sensitive-button { padding: 10px; padding-top: 0; + + .icon-button { + font-size: 14px; + font-weight: 500; + } } .compose-form__warning { From d77ee3f276dc42fb0219ab1b02162f2f1b90257b Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 20:49:27 +0200 Subject: [PATCH 28/54] Fix accounts created through tootctl not being always pre-approved (#10684) Add `--approve` option to `tootctl accounts modify` --- lib/mastodon/accounts_cli.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb index 9dc84f1b52..3131647f35 100644 --- a/lib/mastodon/accounts_cli.rb +++ b/lib/mastodon/accounts_cli.rb @@ -73,7 +73,7 @@ module Mastodon def create(username) account = Account.new(username: username) password = SecureRandom.hex - user = User.new(email: options[:email], password: password, agreement: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil) + user = User.new(email: options[:email], password: password, agreement: true, approved: true, admin: options[:role] == 'admin', moderator: options[:role] == 'moderator', confirmed_at: options[:confirmed] ? Time.now.utc : nil) if options[:reattach] account = Account.find_local(username) || Account.new(username: username) @@ -115,6 +115,7 @@ module Mastodon option :enable, type: :boolean option :disable, type: :boolean option :disable_2fa, type: :boolean + option :approve, type: :boolean desc 'modify USERNAME', 'Modify a user' long_desc <<-LONG_DESC Modify a user account. @@ -128,6 +129,9 @@ module Mastodon With the --disable option, lock the user out of their account. The --enable option is the opposite. + With the --approve option, the account will be approved, if it was + previously not due to not having open registrations. + With the --disable-2fa option, the two-factor authentication requirement for the user can be removed. LONG_DESC @@ -147,6 +151,7 @@ module Mastodon user.email = options[:email] if options[:email] user.disabled = false if options[:enable] user.disabled = true if options[:disable] + user.approved = true if options[:approve] user.otp_required_for_login = false if options[:disable_2fa] user.confirm if options[:confirm] From 7cb369d4c66c4381c856a2714b4117d6204cd4bb Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 23:44:44 +0200 Subject: [PATCH 29/54] Change e-mail whitelist/blacklist to not be checked when invited (#10683) * Change e-mail whitelist/blacklist to not be checked when invited And only when creating an account, not when updating it later Fix #10648 * Fix test --- app/models/user.rb | 2 +- app/validators/blacklisted_email_validator.rb | 5 ++++- spec/validators/blacklisted_email_validator_spec.rb | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 4320786518..bce28aa5fb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -78,7 +78,7 @@ class User < ApplicationRecord accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? } validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? - validates_with BlacklistedEmailValidator, if: :email_changed? + validates_with BlacklistedEmailValidator, on: :create validates_with EmailMxValidator, if: :validate_email_dns? validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create diff --git a/app/validators/blacklisted_email_validator.rb b/app/validators/blacklisted_email_validator.rb index a2061fdd31..a288c20ef4 100644 --- a/app/validators/blacklisted_email_validator.rb +++ b/app/validators/blacklisted_email_validator.rb @@ -2,7 +2,10 @@ class BlacklistedEmailValidator < ActiveModel::Validator def validate(user) + return if user.invited? + @email = user.email + user.errors.add(:email, I18n.t('users.invalid_email')) if blocked_email? end @@ -13,7 +16,7 @@ class BlacklistedEmailValidator < ActiveModel::Validator end def on_blacklist? - return true if EmailDomainBlock.block?(@email) + return true if EmailDomainBlock.block?(@email) return false if Rails.configuration.x.email_domains_blacklist.blank? domains = Rails.configuration.x.email_domains_blacklist.gsub('.', '\.') diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb index d2e442f4ab..84b0107dd7 100644 --- a/spec/validators/blacklisted_email_validator_spec.rb +++ b/spec/validators/blacklisted_email_validator_spec.rb @@ -8,6 +8,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do let(:errors) { double(add: nil) } before do + allow(user).to receive(:invited?) { false } allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email } described_class.new.validate(user) end From b85f216cbcb0a99aac6f7f11e6254452a8f6c3dc Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Fri, 3 May 2019 23:45:37 +0200 Subject: [PATCH 30/54] Do not retry processing ActivityPub jobs raising validation errors (#10614) * Do not retry processing ActivityPub jobs raising validation errors Jobs yielding validation errors most probably won't ever be accepted, so it makes sense not to clutter the queues with retries. * Lower RecordInvalid error reporting to debug log level * Remove trailing whitespace --- app/workers/activitypub/processing_worker.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/workers/activitypub/processing_worker.rb b/app/workers/activitypub/processing_worker.rb index a3abe72cf6..05139f616d 100644 --- a/app/workers/activitypub/processing_worker.rb +++ b/app/workers/activitypub/processing_worker.rb @@ -7,5 +7,7 @@ class ActivityPub::ProcessingWorker def perform(account_id, body, delivered_to_account_id = nil) ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true) + rescue ActiveRecord::RecordInvalid => e + Rails.logger.debug "Error processing incoming ActivityPub object: #{e}" end end From 5f9f610a23c8fac1133250a95de567e68cad183c Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Sat, 4 May 2019 00:31:06 +0200 Subject: [PATCH 31/54] Bump version to 2.8.1 (#10687) --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ lib/mastodon/version.rb | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17626e0270..9639aed086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,45 @@ Changelog All notable changes to this project will be documented in this file. +## [2.8.1] - 2019-05-04 +### Added + +- Add link to existing domain block when trying to block an already-blocked domain ([ThibG](https://github.com/tootsuite/mastodon/pull/10663)) +- Add button to view context to media modal when opened from account gallery in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10676)) +- Add ability to create multiple-choice polls in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/10603)) +- Add `GITHUB_REPOSITORY` and `SOURCE_BASE_URL` environment variables ([rosylilly](https://github.com/tootsuite/mastodon/pull/10600)) +- Add `/interact/` paths to `robots.txt` ([ThibG](https://github.com/tootsuite/mastodon/pull/10666)) +- Add `blurhash` to the Attachment entity in the REST API ([Gargron](https://github.com/tootsuite/mastodon/pull/10630)) + +### Changed + +- Change hidden media to be shown as a blurhash-based colorful gradient instead of a black box in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10630)) +- Change rejected media to be shown as a blurhash-based gradient instead of a list of filenames in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10630)) +- Change e-mail whitelist/blacklist to not be checked when invited ([Gargron](https://github.com/tootsuite/mastodon/pull/10683)) +- Change cache header of REST API results to no-cache ([ThibG](https://github.com/tootsuite/mastodon/pull/10655)) +- Change the "mark media as sensitive" button to be more obvious in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/10673), [Gargron](https://github.com/tootsuite/mastodon/pull/10682)) +- Change account gallery in web UI to display 3 columns, open media modal ([Gargron](https://github.com/tootsuite/mastodon/pull/10667), [Gargron](https://github.com/tootsuite/mastodon/pull/10674)) + +### Fixed + +- Fix LDAP/PAM/SAML/CAS users not being pre-approved ([Gargron](https://github.com/tootsuite/mastodon/pull/10621)) +- Fix accounts created through tootctl not being always pre-approved ([Gargron](https://github.com/tootsuite/mastodon/pull/10684)) +- Fix Sidekiq retrying ActivityPub processing jobs that fail validation ([ThibG](https://github.com/tootsuite/mastodon/pull/10614)) +- Fix toots not being scrolled into view sometimes through keyboard selection ([ThibG](https://github.com/tootsuite/mastodon/pull/10593)) +- Fix expired invite links being usable to bypass approval mode ([ThibG](https://github.com/tootsuite/mastodon/pull/10657)) +- Fix not being able to save e-mail preference for new pending accounts ([Gargron](https://github.com/tootsuite/mastodon/pull/10622)) +- Fix upload progressbar when image resizing is involved ([ThibG](https://github.com/tootsuite/mastodon/pull/10632)) +- Fix block action not automatically cancelling pending follow request ([ThibG](https://github.com/tootsuite/mastodon/pull/10633)) +- Fix stoplight logging to stderr separate from Rails logger ([Gargron](https://github.com/tootsuite/mastodon/pull/10624)) +- Fix sign up button not saying sign up when invite is used ([Gargron](https://github.com/tootsuite/mastodon/pull/10623)) +- Fix health checks in Docker Compose configuration ([fabianonline](https://github.com/tootsuite/mastodon/pull/10553)) +- Fix modal items not being scrollable on touch devices ([kedamaDQ](https://github.com/tootsuite/mastodon/pull/10605)) +- Fix Keybase configuration using wrong domain when a web domain is used ([BenLubar](https://github.com/tootsuite/mastodon/pull/10565)) +- Fix avatar GIFs not being animated on-hover on public profiles ([hyenagirl64](https://github.com/tootsuite/mastodon/pull/10549)) +- Fix OpenGraph parser not understanding some valid property meta tags ([da2x](https://github.com/tootsuite/mastodon/pull/10604)) +- Fix wrong fonts being displayed when Roboto is installed on user's machine ([ThibG](https://github.com/tootsuite/mastodon/pull/10594)) +- Fix confirmation modals being too narrow for a secondary action button ([ThibG](https://github.com/tootsuite/mastodon/pull/10586)) + ## [2.8.0] - 2019-04-10 ### Added diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index a656031b11..fecfe8e709 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 0 + 1 end def pre From 8025a41a1ff2ae1cd7c36a2baac36d3e2badb75d Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Sat, 4 May 2019 01:02:57 +0200 Subject: [PATCH 32/54] Add `tootctl cache clear` (#10689) --- lib/cli.rb | 4 ++++ lib/mastodon/cache_cli.rb | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 lib/mastodon/cache_cli.rb diff --git a/lib/cli.rb b/lib/cli.rb index b56c6e76f5..5780e3e873 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -9,6 +9,7 @@ require_relative 'mastodon/search_cli' require_relative 'mastodon/settings_cli' require_relative 'mastodon/statuses_cli' require_relative 'mastodon/domains_cli' +require_relative 'mastodon/cache_cli' require_relative 'mastodon/version' module Mastodon @@ -41,6 +42,9 @@ module Mastodon desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains' subcommand 'domains', Mastodon::DomainsCLI + desc 'cache SUBCOMMAND ...ARGS', 'Manage cache' + subcommand 'cache', Mastodon::CacheCLI + option :dry_run, type: :boolean desc 'self-destruct', 'Erase the server from the federation' long_desc <<~LONG_DESC diff --git a/lib/mastodon/cache_cli.rb b/lib/mastodon/cache_cli.rb new file mode 100644 index 0000000000..e9b6667b3b --- /dev/null +++ b/lib/mastodon/cache_cli.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative '../../config/boot' +require_relative '../../config/environment' +require_relative 'cli_helper' + +module Mastodon + class CacheCLI < Thor + def self.exit_on_failure? + true + end + + desc 'clear', 'Clear out the cache storage' + def clear + Rails.cache.clear + say('OK', :green) + end + end +end From c88d9e524b02cba895de9bdb7cba0e29e37703d5 Mon Sep 17 00:00:00 2001 From: Alix Rossi <a.d.r@alixrossi.corsica> Date: Sat, 4 May 2019 13:09:25 +0200 Subject: [PATCH 33/54] i18n: Update Corsican translation (#10692) --- config/locales/co.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/co.yml b/config/locales/co.yml index aa68336f16..8c1a13e54b 100644 --- a/config/locales/co.yml +++ b/config/locales/co.yml @@ -269,6 +269,7 @@ co: created_msg: U blucchime di u duminiu hè attivu destroyed_msg: U blucchime di u duminiu ùn hè più attivu domain: Duminiu + existing_domain_block_html: Avete digià impostu limite più strette nant'à %{name}, duvete <a href="%{unblock_url}">sbluccallu</a> primu. new: create: Creà un blucchime hint: U blucchime di duminiu ùn impedirà micca a creazione di conti indè a database, mà metudi di muderazione specifiche saranu applicati. From 1149ddd3da0ca94d5dcdf3b58ca9424a5e34fc25 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Fri, 3 May 2019 20:36:36 +0200 Subject: [PATCH 34/54] Fix glitch SCSS for links in error messages in admin interface Port SCSS changes from 011b032300657ccca4a42866749afc6ec2588ecc --- app/javascript/flavours/glitch/styles/forms.scss | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 91888d3059..2b8d7a6825 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -533,6 +533,17 @@ code { color: $error-value-color; } + a { + display: inline-block; + color: $darker-text-color; + text-decoration: none; + + &:hover { + color: $primary-text-color; + text-decoration: underline; + } + } + p { margin-bottom: 15px; } From 4f73cde4e136320e9325d7f43232c5a614874388 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Sat, 4 May 2019 17:36:43 +0200 Subject: [PATCH 35/54] Minor account media gallery fixes (#10695) * Make the cursor icon consistant across media types in account media gallery * Fix the video player modal causing scroll position to reset --- .../features/account_gallery/components/media_item.js | 2 +- app/javascript/mastodon/features/ui/index.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js index 8d462996ed..5643e64490 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.js +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js @@ -133,7 +133,7 @@ export default class MediaItem extends ImmutablePureComponent { return ( <div className='account-gallery__item' style={{ width, height }}> - <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' style={{ cursor: 'pointer' }} onClick={this.handleClick}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> {visible && thumbnail} </a> diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index c14eba9925..1fcea779d6 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -47,7 +47,8 @@ import { Lists, } from './util/async-components'; import { me } from '../../initial_state'; -import { previewState } from './components/media_modal'; +import { previewState as previewMediaState } from './components/media_modal'; +import { previewState as previewVideoState } from './components/video_modal'; // Dummy import, to make sure that <Status /> ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. @@ -121,7 +122,7 @@ class SwitchingColumnsArea extends React.PureComponent { } shouldUpdateScroll (_, { location }) { - return location.state !== previewState; + return location.state !== previewMediaState && location.state !== previewVideoState; } handleResize = debounce(() => { From 56880fa76afa167aaca028faa3c3267b6521e0bc Mon Sep 17 00:00:00 2001 From: Ushitora Anqou <ushitora@anqou.net> Date: Sun, 5 May 2019 00:39:17 +0900 Subject: [PATCH 36/54] Add SOURCE_TAG to show source repository's tag (#10698) --- lib/mastodon/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index fecfe8e709..127c510485 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -42,7 +42,7 @@ module Mastodon # specify git tag or commit hash here def source_tag - nil + ENV.fetch('SOURCE_TAG') { nil } end def source_url From 7aa749ab46b53bc5b234332ac35acc09a636fc28 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Sat, 4 May 2019 17:39:53 +0200 Subject: [PATCH 37/54] Fix transition: all (#10699) --- app/javascript/styles/mastodon/admin.scss | 2 ++ app/javascript/styles/mastodon/components.scss | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index fd5c08f043..dd3b47a8fa 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -50,6 +50,7 @@ $content-width: 840px; color: $darker-text-color; text-decoration: none; transition: all 200ms linear; + transition-property: color, background-color; border-radius: 4px 0 0 4px; i.fa { @@ -60,6 +61,7 @@ $content-width: 840px; color: $primary-text-color; background-color: darken($ui-base-color, 5%); transition: all 100ms linear; + transition-property: color, background-color; } &.selected { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2326dee38b..cf8fa9392b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1972,6 +1972,7 @@ a.account__display-name { font-weight: 500; border-bottom: 2px solid lighten($ui-base-color, 8%); transition: all 50ms linear; + transition-property: border-bottom, background, color; .fa { font-weight: 400; @@ -2137,7 +2138,7 @@ a.account__display-name { padding: 0; border-radius: 30px; background-color: $ui-base-color; - transition: all 0.2s ease; + transition: background-color 0.2s ease; } .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { @@ -2190,7 +2191,6 @@ a.account__display-name { } .react-toggle-thumb { - transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; position: absolute; top: 1px; left: 1px; @@ -2201,6 +2201,7 @@ a.account__display-name { background-color: darken($simple-background-color, 2%); box-sizing: border-box; transition: all 0.25s ease; + transition-property: border-color, left; } .react-toggle--checked .react-toggle-thumb { @@ -3552,6 +3553,7 @@ a.status-card.compact:hover { display: inline-block; opacity: 0; transition: all 100ms linear; + transition-property: transform, opacity; font-size: 18px; width: 18px; height: 18px; From ccf4f3240ae80f4b1410d816f03d0bef33062a71 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Sat, 27 Apr 2019 03:24:09 +0200 Subject: [PATCH 38/54] [Glitch] Add blurhash Port front-end changes from fba96c808d25d2fc35ec63ee6745a1e55a95d707 to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- .../glitch/components/media_gallery.js | 107 ++++++++++++------ .../flavours/glitch/components/status.js | 1 + .../report/components/status_check_box.js | 1 + .../status/components/detailed_status.js | 1 + .../features/ui/components/media_modal.js | 1 + .../features/ui/components/video_modal.js | 1 + .../flavours/glitch/features/video/index.js | 48 ++++++-- .../glitch/styles/components/index.scss | 44 ++++++- .../glitch/styles/components/media.scss | 17 +++ .../glitch/styles/components/status.scss | 9 +- 10 files changed, 179 insertions(+), 51 deletions(-) diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index b7360bae47..d1dde45b1a 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -7,6 +7,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from 'flavours/glitch/util/is_mobile'; import classNames from 'classnames'; import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import { decode } from 'blurhash'; const messages = defineMessages({ hidden: { @@ -41,6 +42,7 @@ class Item extends React.PureComponent { letterbox: PropTypes.bool, onClick: PropTypes.func.isRequired, displayWidth: PropTypes.number, + visible: PropTypes.bool.isRequired, }; static defaultProps = { @@ -49,6 +51,10 @@ class Item extends React.PureComponent { size: 1, }; + state = { + loaded: false, + }; + handleMouseEnter = (e) => { if (this.hoverToPlay()) { e.target.play(); @@ -82,8 +88,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, letterbox, displayWidth } = this.props; + const { attachment, index, size, standalone, letterbox, displayWidth, visible } = this.props; let width = 50; let height = 100; @@ -136,12 +174,20 @@ class Item extends React.PureComponent { let thumbnail = ''; - if (attachment.get('type') === 'image') { + if (attachment.get('type') === 'unknown') { + return ( + <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} > + <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> + </a> + </div> + ); + } 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'; @@ -168,6 +214,7 @@ class Item extends React.PureComponent { alt={attachment.get('description')} title={attachment.get('description')} style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }} + onLoad={this.handleImageLoad} /> </a> ); @@ -197,7 +244,8 @@ class Item extends React.PureComponent { return ( <div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - {thumbnail} + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} /> + {visible && thumbnail} </div> ); } @@ -257,6 +305,7 @@ export default class MediaGallery extends React.PureComponent { this.node = node; if (node && node.offsetWidth && node.offsetWidth != this.state.width) { if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth); + this.setState({ width: node.offsetWidth, }); @@ -275,7 +324,7 @@ export default class MediaGallery extends React.PureComponent { const width = this.state.width || defaultWidth; - let children; + let children, spoilerButton; const style = {}; @@ -289,40 +338,32 @@ export default class MediaGallery extends React.PureComponent { return (<div className={computedClass} ref={this.handleRef}></div>); } - if (!visible) { - let warning = <FormattedMessage {...(sensitive ? messages.warning : messages.hidden)} />; + if (this.isStandaloneEligible()) { + children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; + } else { + children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible} />); + } - children = ( - <button className='media-spoiler' type='button' onClick={this.handleOpen}> - <span className='media-spoiler__warning'>{warning}</span> - <span className='media-spoiler__trigger'><FormattedMessage {...messages.toggle} /></span> + if (visible) { + spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />; + } else { + spoilerButton = ( + <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> + <span className='spoiler-button__overlay__label'>{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}</span> </button> ); - } else { - if (this.isStandaloneEligible()) { - children = <Item standalone attachment={media.get(0)} onClick={this.handleClick} displayWidth={width} />; - } else { - children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} />); - } } return ( <div className={computedClass} style={style} ref={this.handleRef}> - {visible ? ( - <div className='sensitive-info'> - <IconButton - icon='eye' - onClick={this.handleOpen} - overlay - title={intl.formatMessage(messages.toggle_visible)} - /> - {sensitive ? ( - <span className='sensitive-marker'> - <FormattedMessage {...messages.sensitive} /> - </span> - ) : null} - </div> - ) : null} + <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}> + {spoilerButton} + {visible && sensitive && ( + <span className='sensitive-marker'> + <FormattedMessage {...messages.sensitive} /> + </span> + )} + </div> {children} </div> diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 94297260b3..5f10e0c52b 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -479,6 +479,7 @@ export default class Status extends ImmutablePureComponent { <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > {Component => (<Component preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} inline diff --git a/app/javascript/flavours/glitch/features/report/components/status_check_box.js b/app/javascript/flavours/glitch/features/report/components/status_check_box.js index d674eecf94..cc49042fca 100644 --- a/app/javascript/flavours/glitch/features/report/components/status_check_box.js +++ b/app/javascript/flavours/glitch/features/report/components/status_check_box.js @@ -35,6 +35,7 @@ export default class StatusCheckBox extends React.PureComponent { {Component => ( <Component preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} width={239} 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 8b35de0d49..0b162ed365 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -134,6 +134,7 @@ export default class DetailedStatus extends ImmutablePureComponent { media = ( <Video preview={video.get('preview_url')} + blurhash={video.get('blurhash')} src={video.get('url')} alt={video.get('description')} inline diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index 1f3ac18eaf..39386ee1c3 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -123,6 +123,7 @@ export default class MediaModal extends ImmutablePureComponent { return ( <Video preview={image.get('preview_url')} + blurhash={image.get('blurhash')} src={image.get('url')} width={image.get('width')} height={image.get('height')} diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index 69e0ee46e2..8c74d5a139 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -20,6 +20,7 @@ export default class VideoModal extends ImmutablePureComponent { <div> <Video preview={media.get('preview_url')} + blurhash={media.get('blurhash')} src={media.get('url')} startTime={time} onCloseVideo={onClose} diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index e3ed799c7e..aad24b1d99 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -6,6 +6,7 @@ import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen'; import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { decode } from 'blurhash'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -104,6 +105,7 @@ export default class Video extends React.PureComponent { preventPlayback: PropTypes.bool, intl: PropTypes.object.isRequired, cacheWidth: PropTypes.func, + blurhash: PropTypes.string, }; state = { @@ -147,6 +149,7 @@ export default class Video extends React.PureComponent { setVideoRef = c => { this.video = c; + if (this.video) { this.setState({ volume: this.video.volume, muted: this.video.muted }); } @@ -160,6 +163,10 @@ export default class Video extends React.PureComponent { this.volume = c; } + setCanvasRef = c => { + this.canvas = c; + } + handleMouseDownRoot = e => { e.preventDefault(); e.stopPropagation(); @@ -181,7 +188,6 @@ export default class Video extends React.PureComponent { } handleVolumeMouseDown = e => { - document.addEventListener('mousemove', this.handleMouseVolSlide, true); document.addEventListener('mouseup', this.handleVolumeMouseUp, true); document.addEventListener('touchmove', this.handleMouseVolSlide, true); @@ -201,7 +207,6 @@ export default class Video extends React.PureComponent { } handleMouseVolSlide = throttle(e => { - const rect = this.volume.getBoundingClientRect(); const x = (e.clientX - rect.left) / this.volWidth; //x position within the element. @@ -272,6 +277,10 @@ export default class Video extends React.PureComponent { document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('mozfullscreenchange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); + + if (this.props.blurhash) { + this._decode(); + } } componentWillUnmount () { @@ -293,6 +302,24 @@ export default class Video extends React.PureComponent { } } + componentDidUpdate (prevProps) { + if (prevProps.blurhash !== this.props.blurhash && this.props.blurhash) { + this._decode(); + } + } + + _decode () { + const hash = this.props.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); + } + } + handleFullscreenChange = () => { this.setState({ fullscreen: isFullscreen() }); } @@ -337,6 +364,7 @@ export default class Video extends React.PureComponent { handleOpenVideo = () => { const { src, preview, width, height, alt } = this.props; + const media = fromJS({ type: 'video', url: src, @@ -385,6 +413,7 @@ export default class Video extends React.PureComponent { } let preload; + if (startTime || fullscreen || dragging) { preload = 'auto'; } else if (detailed) { @@ -403,7 +432,9 @@ export default class Video extends React.PureComponent { onMouseDown={this.handleMouseDownRoot} tabIndex={0} > - <video + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} /> + + {revealed && <video ref={this.setVideoRef} src={src} poster={preview} @@ -423,12 +454,13 @@ export default class Video extends React.PureComponent { onLoadedData={this.handleLoadedData} onProgress={this.handleProgress} onVolumeChange={this.handleVolumeChange} - /> + />} - <button type='button' className={classNames('video-player__spoiler', { active: !revealed })} onClick={this.toggleReveal}> - <span className='video-player__spoiler__title'>{warning}</span> - <span className='video-player__spoiler__subtitle'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> - </button> + <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed })}> + <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> + <span className='spoiler-button__overlay__label'>{warning}</span> + </button> + </div> <div className={classNames('video-player__controls', { active: paused || hovered })}> <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b098676b02..b323305cc2 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1066,15 +1066,49 @@ } .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); + } + } } } diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index fabef2a565..0393a38869 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -117,6 +117,8 @@ text-decoration: none; color: $secondary-text-color; line-height: 0; + position: relative; + z-index: 1; &, img { @@ -131,6 +133,21 @@ } } +.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/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index b656f0baf7..fb031258f9 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -705,7 +705,7 @@ & > div { background: rgba($base-shadow-color, 0.6); - border-radius: 4px; + border-radius: 8px; padding: 12px 9px; flex: 0 0 auto; display: flex; @@ -716,19 +716,18 @@ 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; } } From 373dd1fdf167977c97041059cf009a1331132d28 Mon Sep 17 00:00:00 2001 From: Thibaut Girka <thib@sitedethib.com> Date: Sat, 4 May 2019 18:18:15 +0200 Subject: [PATCH 39/54] Minor CSS fixes --- app/javascript/flavours/glitch/styles/components/index.scss | 3 ++- .../flavours/glitch/styles/components/sensitive.scss | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b323305cc2..788bb2e0ec 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1074,11 +1074,12 @@ z-index: 100; &--minified { - display: block; + display: flex; left: 4px; top: 4px; width: auto; height: auto; + align-items: center; } &--hidden { diff --git a/app/javascript/flavours/glitch/styles/components/sensitive.scss b/app/javascript/flavours/glitch/styles/components/sensitive.scss index b0a7dfe037..44b7ec981e 100644 --- a/app/javascript/flavours/glitch/styles/components/sensitive.scss +++ b/app/javascript/flavours/glitch/styles/components/sensitive.scss @@ -9,13 +9,12 @@ } .sensitive-marker { - margin: 0 3px; border-radius: 2px; padding: 2px 6px; color: rgba($primary-text-color, 0.8); background: rgba($base-overlay-background, 0.5); font-size: 12px; - line-height: 15px; + line-height: 18px; text-transform: uppercase; opacity: .9; transition: opacity .1s ease; From a5da59f140a2a8fb2d3f480cdd87964d0beff103 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Thu, 2 May 2019 08:34:32 +0200 Subject: [PATCH 40/54] [Glitch] Change account gallery in web UI Port 3f143606faa6181ff2745b6bd29ac8ea075088bf to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- .../glitch/components/media_gallery.js | 2 +- .../account_gallery/components/media_item.js | 156 +++++++++++++----- .../glitch/features/account_gallery/index.js | 67 +++++--- .../glitch/styles/components/accounts.scss | 59 +------ .../glitch/styles/components/media.scss | 1 + 5 files changed, 166 insertions(+), 119 deletions(-) diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index d1dde45b1a..ab1cccc601 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -177,7 +177,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return ( <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> - <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }} > + <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> </a> </div> diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 89778e1232..cc35097a79 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -1,69 +1,141 @@ import React from 'react'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Permalink from 'flavours/glitch/components/permalink'; -import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import classNames from 'classnames'; +import { decode } from 'blurhash'; +import { isIOS } from 'flavours/glitch/util/is_mobile'; export default class MediaItem extends ImmutablePureComponent { static propTypes = { - media: ImmutablePropTypes.map.isRequired, + attachment: ImmutablePropTypes.map.isRequired, + displayWidth: PropTypes.number.isRequired, + onOpenMedia: PropTypes.func.isRequired, }; state = { - visible: displayMedia !== 'hide_all' && !this.props.media.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + loaded: false, }; - handleClick = () => { - if (!this.state.visible) { - this.setState({ visible: true }); - return true; + componentDidMount () { + if (this.props.attachment.get('blurhash')) { + this._decode(); } + } - return false; + 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 }); + } + + handleMouseEnter = e => { + if (this.hoverToPlay()) { + e.target.play(); + } + } + + handleMouseLeave = e => { + if (this.hoverToPlay()) { + e.target.pause(); + e.target.currentTime = 0; + } + } + + hoverToPlay () { + return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1; + } + + handleClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + + if (this.state.visible) { + this.props.onOpenMedia(this.props.attachment); + } else { + this.setState({ visible: true }); + } + } } render () { - const { media } = this.props; - const { visible } = this.state; - const status = media.get('status'); - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const style = {}; + const { attachment, displayWidth } = this.props; + const { visible, loaded } = this.state; - let label, icon, title; + const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; + const height = width; + const status = attachment.get('status'); - if (media.get('type') === 'gifv') { - label = <span className='media-gallery__gifv__label'>GIF</span>; - } + let thumbnail = ''; - if (visible) { - style.backgroundImage = `url(${media.get('preview_url')})`; - style.backgroundPosition = `${x}% ${y}%`; - title = media.get('description'); - } else { - icon = ( - <span className='account-gallery__item__icons'> - <i className='fa fa-eye-slash' /> - </span> + if (attachment.get('type') === 'unknown') { + // Skip + } else if (attachment.get('type') === 'image') { + const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0; + const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0; + const x = ((focusX / 2) + .5) * 100; + const y = ((focusY / -2) + .5) * 100; + + thumbnail = ( + <img + src={attachment.get('preview_url')} + alt={attachment.get('description')} + title={attachment.get('description')} + style={{ objectPosition: `${x}% ${y}%` }} + onLoad={this.handleImageLoad} + /> + ); + } else if (['gifv', 'video'].indexOf(attachment.get('type')) !== -1) { + const autoPlay = !isIOS() && autoPlayGif; + + thumbnail = ( + <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> + <video + className='media-gallery__item-gifv-thumbnail' + aria-label={attachment.get('description')} + title={attachment.get('description')} + role='application' + src={attachment.get('url')} + onMouseEnter={this.handleMouseEnter} + onMouseLeave={this.handleMouseLeave} + autoPlay={autoPlay} + loop + muted + /> + <span className='media-gallery__gifv__label'>GIF</span> + </div> ); - title = status.get('spoiler_text') || media.get('description'); } return ( - <div className='account-gallery__item'> - <Permalink - to={`/statuses/${status.get('id')}`} - href={status.get('url')} - style={style} - title={title} - onInterceptClick={this.handleClick} - > - {icon} - {label} - </Permalink> + <div className='account-gallery__item' style={{ width, height }}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' style={{ cursor: 'pointer' }} onClick={this.handleClick}> + <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> + {visible && thumbnail} + </a> </div> ); } diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 3b1af108f3..264aff2616 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -14,12 +14,13 @@ import HeaderContainer from 'flavours/glitch/features/account_timeline/container import { ScrollContainer } from 'react-router-scroll-4'; import LoadMore from 'flavours/glitch/components/load_more'; import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import { openModal } from 'flavours/glitch/actions/modal'; const mapStateToProps = (state, props) => ({ isAccount: !!state.getIn(['accounts', props.params.accountId]), - medias: getAccountGallery(state, props.params.accountId), + attachments: getAccountGallery(state, props.params.accountId), isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']), - hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), + hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']), }); class LoadMoreMedia extends ImmutablePureComponent { @@ -50,12 +51,16 @@ export default class AccountGallery extends ImmutablePureComponent { static propTypes = { params: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, - medias: ImmutablePropTypes.list.isRequired, + attachments: ImmutablePropTypes.list.isRequired, isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, }; + state = { + width: 323, + }; + componentDidMount () { this.props.dispatch(fetchAccount(this.props.params.accountId)); this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId)); @@ -74,11 +79,11 @@ export default class AccountGallery extends ImmutablePureComponent { handleScrollToBottom = () => { if (this.props.hasMore) { - this.handleLoadMore(this.props.medias.size > 0 ? this.props.medias.last().getIn(['status', 'id']) : undefined); + this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined); } } - handleScroll = (e) => { + handleScroll = e => { const { scrollTop, scrollHeight, clientHeight } = e.target; const offset = scrollHeight - scrollTop - clientHeight; @@ -91,7 +96,7 @@ export default class AccountGallery extends ImmutablePureComponent { this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId })); }; - handleLoadOlder = (e) => { + handleLoadOlder = e => { e.preventDefault(); this.handleScrollToBottom(); } @@ -101,12 +106,30 @@ export default class AccountGallery extends ImmutablePureComponent { return !(location.state && location.state.mastodonModalOpen); } - setRef = c => { + setColumnRef = c => { this.column = c; } + handleOpenMedia = attachment => { + if (attachment.get('type') === 'video') { + this.props.dispatch(openModal('VIDEO', { media: attachment })); + } else { + const media = attachment.getIn(['status', 'media_attachments']); + const index = media.findIndex(x => x.get('id') === attachment.get('id')); + + this.props.dispatch(openModal('MEDIA', { media, index })); + } + } + + handleRef = c => { + if (c) { + this.setState({ width: c.offsetWidth }); + } + } + render () { - const { medias, isLoading, hasMore, isAccount } = this.props; + const { attachments, isLoading, hasMore, isAccount } = this.props; + const { width } = this.state; if (!isAccount) { return ( @@ -116,9 +139,7 @@ export default class AccountGallery extends ImmutablePureComponent { ); } - let loadOlder = null; - - if (!medias && isLoading) { + if (!attachments && isLoading) { return ( <Column> <LoadingIndicator /> @@ -126,35 +147,31 @@ export default class AccountGallery extends ImmutablePureComponent { ); } - if (hasMore && !(isLoading && medias.size === 0)) { + let loadOlder = null; + + if (hasMore && !(isLoading && attachments.size === 0)) { loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />; } return ( - <Column ref={this.setRef}> + <Column ref={this.setColumnRef}> <ProfileColumnHeader onClick={this.handleHeaderClick} /> <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable scrollable--flex' onScroll={this.handleScroll}> <HeaderContainer accountId={this.props.params.accountId} /> - <div role='feed' className='account-gallery__container'> - {medias.map((media, index) => media === null ? ( - <LoadMoreMedia - key={'more:' + medias.getIn(index + 1, 'id')} - maxId={index > 0 ? medias.getIn(index - 1, 'id') : null} - onLoadMore={this.handleLoadMore} - /> + <div role='feed' className='account-gallery__container' ref={this.handleRef}> + {attachments.map((attachment, index) => attachment === null ? ( + <LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} /> ) : ( - <MediaItem - key={media.get('id')} - media={media} - /> + <MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} /> ))} + {loadOlder} </div> - {isLoading && medias.size === 0 && ( + {isLoading && attachments.size === 0 && ( <div className='scrollable__append'> <LoadingIndicator /> </div> diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 00380c575c..f753b7efa8 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -331,62 +331,19 @@ .account-gallery__container { display: flex; - justify-content: center; flex-wrap: wrap; - padding: 2px; + justify-content: center; + padding: 4px 2px; } .account-gallery__item { - flex-grow: 1; - width: 50%; - overflow: hidden; + border: none; + box-sizing: border-box; + display: block; position: relative; - - &::before { - content: ""; - display: block; - padding-top: 100%; - } - - a { - display: block; - width: calc(100% - 4px); - height: calc(100% - 4px); - margin: 2px; - top: 0; - left: 0; - background-color: $base-overlay-background; - background-size: cover; - background-position: center; - position: absolute; - color: $ui-primary-color; - text-decoration: none; - border-radius: 4px; - - &:hover, - &:active, - &:focus { - outline: 0; - color: $ui-secondary-color; - - &::before { - content: ""; - display: block; - width: 100%; - height: 100%; - background: rgba($base-overlay-background, 0.3); - border-radius: 4px; - } - } - } - - &__icons { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 24px; - } + border-radius: 4px; + overflow: hidden; + margin: 2px; } .notification__filter-bar, diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 0393a38869..bc241de148 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -58,6 +58,7 @@ pointer-events: none; opacity: 0.9; transition: opacity 0.1s ease; + line-height: 18px; } .media-gallery__gifv { From 209c080280f8dbe7dd02fc328841135b8eade15a Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 04:02:55 +0200 Subject: [PATCH 41/54] [Glitch] Fix alignment of items in the account gallery in web UI and load more per page Port 967e419f8fa87af74f4bb530d7493c1dde02fca8 to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- app/javascript/flavours/glitch/actions/timelines.js | 2 +- app/javascript/flavours/glitch/styles/components/accounts.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index f218ee06b2..cca5715833 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -97,7 +97,7 @@ export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true }); +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index f753b7efa8..518eea5fa2 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -332,7 +332,6 @@ .account-gallery__container { display: flex; flex-wrap: wrap; - justify-content: center; padding: 4px 2px; } From bc97fd641f1f075e45f920fdc214f355ce16f53d Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 16:16:30 +0200 Subject: [PATCH 42/54] [Glitch] Add button to view context to media modal Port eb63217210b0ab85ff1fcca9506d5e7931382a56 to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- .../glitch/features/account_gallery/index.js | 4 +- .../features/ui/components/media_modal.js | 25 ++++++++++- .../features/ui/components/video_modal.js | 18 +++++++- .../flavours/glitch/features/video/index.js | 9 ++-- .../glitch/styles/components/media.scss | 42 +++++++++++++++++++ 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.js index 264aff2616..3e44213067 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.js @@ -112,12 +112,12 @@ export default class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { if (attachment.get('type') === 'video') { - this.props.dispatch(openModal('VIDEO', { media: attachment })); + this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - this.props.dispatch(openModal('MEDIA', { media, index })); + this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status') })); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index 39386ee1c3..ce6660480b 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ExtendedVideoPlayer from 'flavours/glitch/components/extended_video_player'; import classNames from 'classnames'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from 'flavours/glitch/components/icon_button'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; @@ -19,8 +19,13 @@ const messages = defineMessages({ @injectIntl export default class MediaModal extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { media: ImmutablePropTypes.list.isRequired, + status: ImmutablePropTypes.map, index: PropTypes.number.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -81,8 +86,15 @@ export default class MediaModal extends ImmutablePureComponent { })); }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, intl, onClose } = this.props; + const { media, status, intl, onClose } = this.props; const { navigationHidden } = this.state; const index = this.getIndex(); @@ -186,10 +198,19 @@ export default class MediaModal extends ImmutablePureComponent { {content} </ReactSwipeableViews> </div> + <div className={navigationClassName}> <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={40} /> + {leftNav} {rightNav} + + {status && ( + <div className={classNames('media-modal__meta', { 'media-modal__meta--shifted': media.size > 1 })}> + <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> + </div> + )} + <ul className='media-modal__pagination'> {pagination} </ul> diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index 8c74d5a139..3f742c260c 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -3,17 +3,32 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import Video from 'flavours/glitch/features/video'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; export default class VideoModal extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { media: ImmutablePropTypes.map.isRequired, + status: ImmutablePropTypes.map, time: PropTypes.number, onClose: PropTypes.func.isRequired, }; + handleStatusClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); + } + } + render () { - const { media, time, onClose } = this.props; + const { media, status, time, onClose } = this.props; + + const link = status && <a href={status.get('url')} onClick={this.handleStatusClick}><FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a>; return ( <div className='modal-root__modal video-modal'> @@ -24,6 +39,7 @@ export default class VideoModal extends ImmutablePureComponent { src={media.get('url')} startTime={time} onCloseVideo={onClose} + link={link} detailed alt={media.get('description')} /> diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index aad24b1d99..3814858024 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -106,6 +106,7 @@ export default class Video extends React.PureComponent { intl: PropTypes.object.isRequired, cacheWidth: PropTypes.func, blurhash: PropTypes.string, + link: PropTypes.node, }; state = { @@ -384,7 +385,7 @@ export default class Video extends React.PureComponent { } render () { - const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive } = this.props; + const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, link } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = (currentTime / duration) * 100; const playerStyle = {}; @@ -487,13 +488,15 @@ export default class Video extends React.PureComponent { /> </div> - {(detailed || fullscreen) && + {(detailed || fullscreen) && ( <span> <span className='video-player__time-current'>{formatTime(currentTime)}</span> <span className='video-player__time-sep'>/</span> <span className='video-player__time-total'>{formatTime(duration)}</span> </span> - } + )} + + {link && <span className='video-player__link'>{link}</span>} </div> <div className='video-player__buttons right'> diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index bc241de148..e5927057ee 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -270,6 +270,31 @@ pointer-events: none; } +.media-modal__meta { + text-align: center; + position: absolute; + left: 0; + bottom: 20px; + width: 100%; + pointer-events: none; + + &--shifted { + bottom: 62px; + } + + a { + text-decoration: none; + font-weight: 500; + color: $ui-secondary-color; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } +} + .media-modal__page-dot { display: inline-block; } @@ -519,6 +544,23 @@ } } + &__link { + padding: 2px 10px; + + a { + text-decoration: none; + font-size: 14px; + font-weight: 500; + color: $white; + + &:hover, + &:active, + &:focus { + text-decoration: underline; + } + } + } + &__seek { cursor: pointer; height: 24px; From b7f69beebe2a4f7b94bc39772e7e9714540220b7 Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Sat, 4 May 2019 17:36:43 +0200 Subject: [PATCH 43/54] [Glitch] Make the cursor icon consistant across media types in account media gallery --- .../glitch/features/account_gallery/components/media_item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index cc35097a79..1eae552f66 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -132,7 +132,7 @@ export default class MediaItem extends ImmutablePureComponent { return ( <div className='account-gallery__item' style={{ width, height }}> - <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' style={{ cursor: 'pointer' }} onClick={this.handleClick}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> {visible && thumbnail} </a> From 8e221cd22bf272f7de8c12e06a98bfb7226a4f1c Mon Sep 17 00:00:00 2001 From: ThibG <thib@sitedethib.com> Date: Sat, 4 May 2019 17:39:53 +0200 Subject: [PATCH 44/54] [Glitch] Fix transition: all Port 7aa749ab46b53bc5b234332ac35acc09a636fc28 to glitch-soc --- app/javascript/flavours/glitch/styles/admin.scss | 2 ++ .../flavours/glitch/styles/components/drawer.scss | 1 + .../flavours/glitch/styles/components/index.scss | 8 ++++++-- .../flavours/glitch/styles/components/search.scss | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 05c7821e44..74f91599a6 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -50,6 +50,7 @@ $content-width: 840px; color: $darker-text-color; text-decoration: none; transition: all 200ms linear; + transition-property: color, background-color; border-radius: 4px 0 0 4px; i.fa { @@ -60,6 +61,7 @@ $content-width: 840px; color: $primary-text-color; background-color: darken($ui-base-color, 5%); transition: all 100ms linear; + transition-property: color, background-color; } &.selected { diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 41c7947901..9f426448fb 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -125,6 +125,7 @@ cursor: default; pointer-events: none; transition: all 100ms linear; + transition-property: color, transform, opacity; } .fa-search { diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 788bb2e0ec..f12f8b7faf 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -25,6 +25,7 @@ text-decoration: none; text-overflow: ellipsis; transition: all 100ms ease-in; + transition-property: background-color; white-space: nowrap; width: auto; @@ -33,6 +34,7 @@ &:hover { background-color: lighten($ui-highlight-color, 7%); transition: all 200ms ease-out; + transition-property: background-color; } &--destructive { @@ -564,6 +566,7 @@ font-weight: 500; border-bottom: 2px solid lighten($ui-base-color, 8%); transition: all 200ms linear; + transition-property: background; .fa { font-weight: 400; @@ -581,6 +584,7 @@ @include multi-columns('screen and (min-width: 631px)') { background: lighten($ui-base-color, 14%); transition: all 100ms linear; + transition-property: background; } } @@ -664,7 +668,7 @@ padding: 0; border-radius: 30px; background-color: $ui-base-color; - transition: all 0.2s ease; + transition: background-color 0.2s ease; } .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { @@ -717,7 +721,6 @@ } .react-toggle-thumb { - transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1) 0ms; position: absolute; top: 1px; left: 1px; @@ -728,6 +731,7 @@ background-color: darken($simple-background-color, 2%); box-sizing: border-box; transition: all 0.25s ease; + transition-property: border-color, left; } .react-toggle--checked .react-toggle-thumb { diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 3746fbad20..f59ef019e8 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -18,6 +18,7 @@ display: inline-block; opacity: 0; transition: all 100ms linear; + transition-property: transform, opacity; font-size: 18px; width: 18px; height: 18px; From cbda1b8b66270a02a3d06d1cafe0c6396466c50d Mon Sep 17 00:00:00 2001 From: Thibaut Girka <thib@sitedethib.com> Date: Sat, 4 May 2019 20:03:37 +0200 Subject: [PATCH 45/54] Add back description on hover --- .../glitch/features/account_gallery/components/media_item.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index 1eae552f66..f2a661862f 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -88,6 +88,7 @@ export default class MediaItem extends ImmutablePureComponent { const width = `${Math.floor((displayWidth - 4) / 3) - 4}px`; const height = width; const status = attachment.get('status'); + const title = status.get('spoiler_text') || attachment.get('description'); let thumbnail = ''; @@ -132,7 +133,7 @@ export default class MediaItem extends ImmutablePureComponent { return ( <div className='account-gallery__item' style={{ width, height }}> - <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick}> + <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> {visible && thumbnail} </a> From b1ab4d5ebe3ccb7d91e55eedd0ad08226358c446 Mon Sep 17 00:00:00 2001 From: Thibaut Girka <thib@sitedethib.com> Date: Sat, 4 May 2019 20:06:17 +0200 Subject: [PATCH 46/54] Add visibility icon back in media gallery --- .../features/account_gallery/components/media_item.js | 8 +++++++- .../flavours/glitch/styles/components/accounts.scss | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index f2a661862f..026136b2c2 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -131,11 +131,17 @@ export default class MediaItem extends ImmutablePureComponent { ); } + const icon = ( + <span className='account-gallery__item__icons'> + <i className='fa fa-eye-slash' /> + </span> + ); + return ( <div className='account-gallery__item' style={{ width, height }}> <a className='media-gallery__item-thumbnail' href={status.get('url')} target='_blank' onClick={this.handleClick} title={title}> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && loaded })} /> - {visible && thumbnail} + {visible ? thumbnail : icon} </a> </div> ); diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 518eea5fa2..c0340e3f83 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -343,6 +343,14 @@ border-radius: 4px; overflow: hidden; margin: 2px; + + &__icons { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 24px; + } } .notification__filter-bar, From f59973cc85d9e84bd484ca7c75f108ccbb5d17df Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 04:34:55 +0200 Subject: [PATCH 47/54] [Glitch] Make the "mark media as sensitive" button more obvious in web UI Port 05ef3462ba0af7b147a7cfa8de2735e99dc59ac5 to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- .../glitch/components/media_gallery.js | 2 +- .../features/compose/components/options.js | 41 -------------- .../compose/components/upload_form.js | 3 ++ .../compose/containers/options_container.js | 5 -- .../containers/sensitive_button_container.js | 54 +++++++++++++++++++ .../flavours/glitch/features/video/index.js | 2 +- .../glitch/styles/components/composer.scss | 5 ++ 7 files changed, 64 insertions(+), 48 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index ab1cccc601..194800d527 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -345,7 +345,7 @@ export default class MediaGallery extends React.PureComponent { } if (visible) { - spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon={visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />; + spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />; } else { spoilerButton = ( <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 8a760bd15d..ee97309616 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -65,10 +65,6 @@ const messages = defineMessages({ defaultMessage: 'Public', id: 'privacy.public.short', }, - sensitive: { - defaultMessage: 'Mark media as sensitive', - id: 'compose_form.sensitive', - }, spoiler: { defaultMessage: 'Hide text behind warning', id: 'compose_form.spoiler', @@ -116,7 +112,6 @@ class ComposerOptions extends ImmutablePureComponent { hasPoll: PropTypes.bool, intl: PropTypes.object.isRequired, onChangeAdvancedOption: PropTypes.func, - onChangeSensitivity: PropTypes.func, onChangeVisibility: PropTypes.func, onTogglePoll: PropTypes.func, onDoodleOpen: PropTypes.func, @@ -126,7 +121,6 @@ class ComposerOptions extends ImmutablePureComponent { onUpload: PropTypes.func, privacy: PropTypes.string, resetFileKey: PropTypes.number, - sensitive: PropTypes.bool, spoiler: PropTypes.bool, }; @@ -175,7 +169,6 @@ class ComposerOptions extends ImmutablePureComponent { hasPoll, intl, onChangeAdvancedOption, - onChangeSensitivity, onChangeVisibility, onTogglePoll, onModalClose, @@ -183,7 +176,6 @@ class ComposerOptions extends ImmutablePureComponent { onToggleSpoiler, privacy, resetFileKey, - sensitive, spoiler, } = this.props; @@ -264,39 +256,6 @@ class ComposerOptions extends ImmutablePureComponent { title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} /> )} - <Motion - defaultStyle={{ scale: 0.87 }} - style={{ - scale: spring(hasMedia ? 1 : 0.87, { - stiffness: 200, - damping: 3, - }), - }} - > - {({ scale }) => ( - <div - style={{ - display: hasMedia ? null : 'none', - transform: `scale(${scale})`, - }} - > - <IconButton - active={sensitive} - className='sensitive' - disabled={spoiler} - icon={sensitive ? 'eye-slash' : 'eye'} - inverted - onClick={onChangeSensitivity} - size={18} - style={{ - height: null, - lineHeight: null, - }} - title={intl.formatMessage(messages.sensitive)} - /> - </div> - )} - </Motion> <hr /> <Dropdown disabled={disabled} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js index 4864043a81..35880ddccc 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.js @@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; +import SensitiveButtonContainer from '../containers/sensitive_button_container'; export default class UploadForm extends ImmutablePureComponent { static propTypes = { @@ -23,6 +24,8 @@ export default class UploadForm extends ImmutablePureComponent { ))} </div> )} + + {!mediaIds.isEmpty() && <SensitiveButtonContainer />} </div> ); } diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index e846cfbd5f..2ac7ab8d8d 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -2,7 +2,6 @@ import { connect } from 'react-redux'; import Options from '../components/options'; import { changeComposeAdvancedOption, - changeComposeSensitivity, } from 'flavours/glitch/actions/compose'; import { addPoll, removePoll } from 'flavours/glitch/actions/compose'; import { closeModal, openModal } from 'flavours/glitch/actions/modal'; @@ -27,10 +26,6 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeComposeAdvancedOption(option, value)); }, - onChangeSensitivity() { - dispatch(changeComposeSensitivity()); - }, - onTogglePoll() { dispatch((_, getState) => { if (getState().getIn(['compose', 'poll'])) { diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js new file mode 100644 index 0000000000..8f163979f8 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, + unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' }, +}); + +const mapStateToProps = state => { + const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); + const spoilerText = state.getIn(['compose', 'spoiler_text']); + return { + active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0), + disabled: state.getIn(['compose', 'spoiler']), + }; +}; + +const mapDispatchToProps = dispatch => ({ + + onClick () { + dispatch(changeComposeSensitivity()); + }, + +}); + +class SensitiveButton extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + render () { + const { active, disabled, onClick, intl } = this.props; + + return ( + <div className='compose-form__sensitive-button'> + <button className={classNames('icon-button', { active })} onClick={onClick} disabled={disabled} title={intl.formatMessage(active ? messages.marked : messages.unmarked)}> + <Icon icon='eye-slash' /> <FormattedMessage id='compose_form.sensitive.hide' defaultMessage='Mark media as sensitive' /> + </button> + </div> + ); + } + +} + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 3814858024..8291ff3c81 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -500,7 +500,7 @@ export default class Video extends React.PureComponent { </div> <div className='video-player__buttons right'> - {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye' /></button>} + {!onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><i className='fa fa-fw fa-eye-slash' /></button>} {(!fullscreen && onOpenVideo) && <button type='button' aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><i className='fa fa-fw fa-expand' /></button>} {onCloseVideo && <button type='button' aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><i className='fa fa-fw fa-compress' /></button>} <button type='button' aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><i className={classNames('fa fa-fw', { 'fa-arrows-alt': !fullscreen, 'fa-compress': fullscreen })} /></button> diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index e5eb6e64fe..81c700737e 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -57,6 +57,11 @@ } } +.compose-form__sensitive-button { + padding: 10px; + padding-top: 0; +} + .composer--reply { margin: 0 0 10px; border-radius: 4px; From 9b1ef58c95f6b0dfa6d22572199fa3cf4337b095 Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Fri, 3 May 2019 20:44:20 +0200 Subject: [PATCH 48/54] [Glitch] Change font weight of sensitive button to 500 Port 63b1388fefff9414c2d0f9883f2d33f7c73284c6 to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- .../flavours/glitch/styles/components/composer.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 81c700737e..86041da200 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -60,6 +60,11 @@ .compose-form__sensitive-button { padding: 10px; padding-top: 0; + + .icon-button { + font-size: 14px; + font-weight: 500; + } } .composer--reply { From f0865171fec447daae1e6d2eb162b3b7bfdfa85c Mon Sep 17 00:00:00 2001 From: Eugen Rochko <eugen@zeonfederated.com> Date: Sat, 4 May 2019 22:52:54 +0200 Subject: [PATCH 49/54] Bump blurhash from 0.1.2 to 0.1.3 (#10700) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7ab907f6d4..59b34a185c 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) - blurhash (0.1.2) + blurhash (0.1.3) ffi (~> 1.10.0) bootsnap (1.4.4) msgpack (~> 1.0) From f2cf8144b427ba0639cea7976718b0ed77fbf183 Mon Sep 17 00:00:00 2001 From: Thibaut Girka <thib@sitedethib.com> Date: Sat, 4 May 2019 23:05:43 +0200 Subject: [PATCH 50/54] Minor style fixes --- app/javascript/flavours/glitch/styles/components/sensitive.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/flavours/glitch/styles/components/sensitive.scss b/app/javascript/flavours/glitch/styles/components/sensitive.scss index 44b7ec981e..67b01c886a 100644 --- a/app/javascript/flavours/glitch/styles/components/sensitive.scss +++ b/app/javascript/flavours/glitch/styles/components/sensitive.scss @@ -9,6 +9,7 @@ } .sensitive-marker { + margin: 0 3px; border-radius: 2px; padding: 2px 6px; color: rgba($primary-text-color, 0.8); From 21209c2b52d1ed1d1dee2dad2d725ffc7714701b Mon Sep 17 00:00:00 2001 From: Baptiste Gelez <baptiste@gelez.xyz> Date: Sun, 5 May 2019 00:07:15 +0100 Subject: [PATCH 51/54] Make sure the instance banner is never cropped (#10702) --- app/javascript/styles/mastodon/widgets.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/styles/mastodon/widgets.scss b/app/javascript/styles/mastodon/widgets.scss index e736d7a7ef..acaf5b0240 100644 --- a/app/javascript/styles/mastodon/widgets.scss +++ b/app/javascript/styles/mastodon/widgets.scss @@ -4,7 +4,6 @@ &__img { width: 100%; - height: 167px; position: relative; overflow: hidden; border-radius: 4px 4px 0 0; From b7741ed732fb8ee580507085d84ec50a6e8d06a2 Mon Sep 17 00:00:00 2001 From: Aditoo17 <42938951+Aditoo17@users.noreply.github.com> Date: Sun, 5 May 2019 08:33:33 +0200 Subject: [PATCH 52/54] =?UTF-8?q?I18n:=20Update=20Czech=20translation=20?= =?UTF-8?q?=F0=9F=87=A8=F0=9F=87=BF=20(#10704)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * I18n: Update Czech translation * Tiny fix --- app/javascript/mastodon/locales/cs.json | 2 ++ config/locales/cs.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index f98ea7f26a..cbf303f3ca 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -77,6 +77,7 @@ "compose_form.poll.remove_option": "Odstranit tuto volbu", "compose_form.publish": "Tootnout", "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive.hide": "Označit média jako citlivá", "compose_form.sensitive.marked": "Média jsou označena jako citlivá", "compose_form.sensitive.unmarked": "Média nejsou označena jako citlivá", "compose_form.spoiler.marked": "Text je skrytý za varováním", @@ -209,6 +210,7 @@ "lightbox.close": "Zavřít", "lightbox.next": "Další", "lightbox.previous": "Předchozí", + "lightbox.view_context": "Zobrazit kontext", "lists.account.add": "Přidat do seznamu", "lists.account.remove": "Odebrat ze seznamu", "lists.delete": "Smazat seznam", diff --git a/config/locales/cs.yml b/config/locales/cs.yml index ca456b7efb..5d05a13d68 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -273,6 +273,7 @@ cs: created_msg: Blokace domény se právě vyřizuje destroyed_msg: Blokace domény byla zrušena domain: Doména + existing_domain_block_html: Pro účet %{name} jste již nastavil/a přísnější omezení, musíte jej nejdříve <a href="%{unblock_url}">odblokovat</a>. new: create: Vytvořit blokaci hint: Blokace domény nezakáže vytváření záznamů účtů v databázi, ale bude na tyto účty zpětně a automaticky aplikovat specifické metody moderování. From fc192b882f43d1dd3d61c68dc3fb327432b9388d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C4=BDach?= <mareklachbc@tutanota.com> Date: Sun, 5 May 2019 10:25:35 +0200 Subject: [PATCH 53/54] Minor Slovak locale update (#10705) --- config/locales/sk.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/locales/sk.yml b/config/locales/sk.yml index b6a966fa7d..d859d16b1e 100644 --- a/config/locales/sk.yml +++ b/config/locales/sk.yml @@ -268,10 +268,11 @@ sk: week_users_active: aktívni tento týždeň week_users_new: užívateľov počas tohto týždňa domain_blocks: - add_new: Pridaj nové doménové blokovanie + add_new: Blokuj novú doménu created_msg: Doména je v štádiu blokovania destroyed_msg: Blokovanie domény bolo zrušené domain: Doména + existing_domain_block_html: Pre účet %{name} si už nahodil/a přísnejšie obmedzenie, najskôr ho teda musíš <a href="%{unblock_url}">odblokovať</a>. new: create: Vytvor blokovanie domény hint: Blokovanie domény stále dovolí vytvárať nové účty v databázi, ale tieto budú spätne automaticky moderované. @@ -299,7 +300,7 @@ sk: silence: Zruš stíšenie všetkých existujúcich účtov z tejto domény suspend: Zruš suspendáciu všetkých existujúcich účtov z tejto domény title: Zruš blokovanie domény %{domain} - undo: Vrátiť späť + undo: Vráť späť undo: Odvolaj blokovanie domény email_domain_blocks: add_new: Pridaj nový From eef8802325d75b2668b131a765c5555f4be8c2ce Mon Sep 17 00:00:00 2001 From: Baptiste Gelez <baptiste@gelez.xyz> Date: Sun, 5 May 2019 00:07:15 +0100 Subject: [PATCH 54/54] [Glitch] Make sure the instance banner is never cropped Port 21209c2b52d1ed1d1dee2dad2d725ffc7714701b to glitch-soc Signed-off-by: Thibaut Girka <thib@sitedethib.com> --- app/javascript/flavours/glitch/styles/widgets.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index e736d7a7ef..acaf5b0240 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -4,7 +4,6 @@ &__img { width: 100%; - height: 167px; position: relative; overflow: hidden; border-radius: 4px 4px 0 0;