diff --git a/CHANGELOG.md b/CHANGELOG.md index 17626e027..9639aed08 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/Gemfile b/Gemfile index eac7a3e43..9deb596c5 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' @@ -66,7 +67,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' @@ -124,14 +125,14 @@ 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' - 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 + 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 b4701868c..76d6224ea 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) @@ -99,12 +99,14 @@ GEM rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.4.3) + blurhash (0.1.2) + ffi (~> 1.10.0) + bootsnap (1.4.4) msgpack (~> 1.0) 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) @@ -205,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) @@ -348,7 +350,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) @@ -395,7 +397,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,14 +422,13 @@ 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) 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) @@ -472,8 +473,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) @@ -528,11 +529,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.1) 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) @@ -546,12 +546,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) @@ -663,10 +663,11 @@ 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 - bullet (~> 5.9) + bullet (~> 6.0) bundler-audit (~> 0.6) capistrano (~> 3.11) capistrano-rails (~> 1.4) @@ -737,7 +738,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) @@ -750,9 +751,9 @@ 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) + scss_lint (~> 0.58) sidekiq (~> 5.2) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 5f307ddee..dd3f83389 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/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 3a92ee4e4..eca558f42 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 7bac27da4..1bb19a09d 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 e14e0aee8..09edfe365 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 2070c487d..a8891d126 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 5686e8d7c..8c83a1801 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 diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 84099bd96..c56728464 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -96,7 +96,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/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb index 68ebddfc9..7d7e237fb 100644 --- a/app/controllers/settings/notifications_controller.rb +++ b/app/controllers/settings/notifications_controller.rb @@ -21,7 +21,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 diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index d65d41048..0ee663766 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 }){ diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index d92385e95..06c21b96b 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/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index a2bc95255..abd17647e 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 ( +