diff --git a/Gemfile.lock b/Gemfile.lock
index b47ab00911..c3269e108b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -93,7 +93,7 @@ GEM
cocaine (~> 0.5.3)
aws-eventstream (1.1.0)
aws-partitions (1.329.0)
- aws-sdk-core (3.99.2)
+ aws-sdk-core (3.100.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb
index b8bca580f6..5c8cdd1745 100644
--- a/app/controllers/accounts_controller.rb
+++ b/app/controllers/accounts_controller.rb
@@ -11,7 +11,7 @@ class AccountsController < ApplicationController
before_action :set_body_classes
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def show
respond_to do |format|
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 153ade253d..045e7dd266 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders
skip_before_action :store_current_location
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index d1384ed56f..fe1142f345 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -7,8 +7,6 @@ module Localized
around_action :set_locale
end
- private
-
def set_locale
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
locale ||= session[:locale] ||= default_locale
@@ -19,6 +17,8 @@ module Localized
end
end
+ private
+
def default_locale
if ENV['DEFAULT_LOCALE'].present?
I18n.default_locale
diff --git a/app/controllers/concerns/sign_in_token_authentication_concern.rb b/app/controllers/concerns/sign_in_token_authentication_concern.rb
index 88c009b19d..f5178930b6 100644
--- a/app/controllers/concerns/sign_in_token_authentication_concern.rb
+++ b/app/controllers/concerns/sign_in_token_authentication_concern.rb
@@ -42,9 +42,11 @@ module SignInTokenAuthenticationConcern
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
end
- session[:attempt_user_id] = user.id
- use_pack 'auth'
- @body_classes = 'lighter'
- render :sign_in_token
+ set_locale do
+ session[:attempt_user_id] = user.id
+ use_pack 'auth'
+ @body_classes = 'lighter'
+ render :sign_in_token
+ end
end
end
diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb
index 0d9f874551..35c0c27cfc 100644
--- a/app/controllers/concerns/two_factor_authentication_concern.rb
+++ b/app/controllers/concerns/two_factor_authentication_concern.rb
@@ -40,9 +40,11 @@ module TwoFactorAuthenticationConcern
end
def prompt_for_two_factor(user)
- session[:attempt_user_id] = user.id
- use_pack 'auth'
- @body_classes = 'lighter'
- render :two_factor
+ set_locale do
+ session[:attempt_user_id] = user.id
+ use_pack 'auth'
+ @body_classes = 'lighter'
+ render :two_factor
+ end
end
end
diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb
index adf2bd0146..549c6a39e0 100644
--- a/app/controllers/directories_controller.rb
+++ b/app/controllers/directories_controller.rb
@@ -10,7 +10,7 @@ class DirectoriesController < ApplicationController
before_action :set_accounts
before_action :set_pack
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def index
render :index
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index eb223c3f7c..5ffbdae79d 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def index
respond_to do |format|
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 4ddccf607b..69820ebb7d 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def index
respond_to do |format|
diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb
index 1d166d6e73..ce015dd1b2 100644
--- a/app/controllers/media_controller.rb
+++ b/app/controllers/media_controller.rb
@@ -4,7 +4,7 @@ class MediaController < ApplicationController
include Authorization
skip_before_action :store_current_location
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_media_attachment
diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb
index 51bb9bdeaa..a277bfa103 100644
--- a/app/controllers/remote_interaction_controller.rb
+++ b/app/controllers/remote_interaction_controller.rb
@@ -11,7 +11,7 @@ class RemoteInteractionController < ApplicationController
before_action :set_body_classes
before_action :set_pack
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def new
@remote_follow = RemoteFollow.new(session_params)
diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb
index b0abad984f..a6ab8828f2 100644
--- a/app/controllers/statuses_controller.rb
+++ b/app/controllers/statuses_controller.rb
@@ -19,7 +19,7 @@ class StatusesController < ApplicationController
before_action :set_autoplay, only: :embed
skip_around_action :set_locale, if: -> { request.format == :json }
- skip_before_action :require_functional!, only: [:show, :embed]
+ skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
content_security_policy only: :embed do |p|
p.frame_ancestors(false)
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 2363cb31b9..e46c0532cb 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -15,7 +15,7 @@ class TagsController < ApplicationController
before_action :set_body_classes
before_action :set_instance_presenter
- skip_before_action :require_functional!
+ skip_before_action :require_functional!, unless: :whitelist_mode?
def show
respond_to do |format|
diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js
index 14e5cb94a7..d0d9714a8b 100644
--- a/app/javascript/flavours/glitch/features/emoji_picker/index.js
+++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js
@@ -283,7 +283,7 @@ class EmojiPickerMenu extends React.PureComponent {
if (!emoji.native) {
emoji.native = emoji.colons;
}
- if (!event.ctrlKey) {
+ if (!(event.ctrlKey || event.metaKey)) {
this.props.onClose();
}
this.props.onPick(emoji);
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss
index dbf0c908d1..772b40dc45 100644
--- a/app/javascript/flavours/glitch/styles/components/media.scss
+++ b/app/javascript/flavours/glitch/styles/components/media.scss
@@ -76,7 +76,7 @@
border-radius: 4px;
position: relative;
width: 100%;
- height: 110px;
+ min-height: 64px;
@include fullwidth-gallery;
}
@@ -404,6 +404,7 @@
@include fullwidth-gallery;
video {
+ display: block;
max-width: 100vw;
max-height: 80vh;
z-index: 1;
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js
index a31de206b4..0ec8661380 100644
--- a/app/javascript/mastodon/components/media_gallery.js
+++ b/app/javascript/mastodon/components/media_gallery.js
@@ -8,10 +8,10 @@ import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
import { decode } from 'blurhash';
+import { debounce } from 'lodash';
const messages = defineMessages({
- toggle_visible: { id: 'media_gallery.toggle_visible',
- defaultMessage: 'Hide {number, plural, one {image} other {images}}' },
+ toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide {number, plural, one {image} other {images}}' },
});
class Item extends React.PureComponent {
@@ -267,6 +267,14 @@ class MediaGallery extends React.PureComponent {
width: this.props.defaultWidth,
};
+ componentDidMount () {
+ window.addEventListener('resize', this.handleResize, { passive: true });
+ }
+
+ componentWillUnmount () {
+ window.removeEventListener('resize', this.handleResize);
+ }
+
componentWillReceiveProps (nextProps) {
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
@@ -275,6 +283,14 @@ class MediaGallery extends React.PureComponent {
}
}
+ handleResize = debounce(() => {
+ if (this.node) {
+ this._setDimensions();
+ }
+ }, 250, {
+ trailing: true,
+ });
+
handleOpen = () => {
if (this.props.onToggleVisibility) {
this.props.onToggleVisibility();
@@ -287,17 +303,27 @@ class MediaGallery extends React.PureComponent {
this.props.onOpenMedia(this.props.media, index);
}
- handleRef = (node) => {
- if (node) {
- // offsetWidth triggers a layout, so only calculate when we need to
- if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
+ handleRef = c => {
+ this.node = c;
- this.setState({
- width: node.offsetWidth,
- });
+ if (this.node) {
+ this._setDimensions();
}
}
+ _setDimensions () {
+ const width = this.node.offsetWidth;
+
+ // offsetWidth triggers a layout, so only calculate when we need to
+ if (this.props.cacheWidth) {
+ this.props.cacheWidth(width);
+ }
+
+ this.setState({
+ width: width,
+ });
+ }
+
isFullSizeEligible() {
const { media } = this.props;
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index f99ccd39a6..5f42534bac 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -345,9 +345,11 @@ class Status extends ImmutablePureComponent {
)}
diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js
index baad1c0e56..9da143c96e 100644
--- a/app/javascript/mastodon/features/audio/index.js
+++ b/app/javascript/mastodon/features/audio/index.js
@@ -1,11 +1,136 @@
import React from 'react';
import PropTypes from 'prop-types';
-import WaveSurfer from 'wavesurfer.js';
import { defineMessages, injectIntl } from 'react-intl';
import { formatTime } from 'mastodon/features/video';
import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
import { throttle } from 'lodash';
+import { encode, decode } from 'blurhash';
+import { getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
+import { debounce } from 'lodash';
+
+const digitCharacters = [
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ 'A',
+ 'B',
+ 'C',
+ 'D',
+ 'E',
+ 'F',
+ 'G',
+ 'H',
+ 'I',
+ 'J',
+ 'K',
+ 'L',
+ 'M',
+ 'N',
+ 'O',
+ 'P',
+ 'Q',
+ 'R',
+ 'S',
+ 'T',
+ 'U',
+ 'V',
+ 'W',
+ 'X',
+ 'Y',
+ 'Z',
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ 'g',
+ 'h',
+ 'i',
+ 'j',
+ 'k',
+ 'l',
+ 'm',
+ 'n',
+ 'o',
+ 'p',
+ 'q',
+ 'r',
+ 's',
+ 't',
+ 'u',
+ 'v',
+ 'w',
+ 'x',
+ 'y',
+ 'z',
+ '#',
+ '$',
+ '%',
+ '*',
+ '+',
+ ',',
+ '-',
+ '.',
+ ':',
+ ';',
+ '=',
+ '?',
+ '@',
+ '[',
+ ']',
+ '^',
+ '_',
+ '{',
+ '|',
+ '}',
+ '~',
+];
+
+const decode83 = (str) => {
+ let value = 0;
+ let c, digit;
+
+ for (let i = 0; i < str.length; i++) {
+ c = str[i];
+ digit = digitCharacters.indexOf(c);
+ value = value * 83 + digit;
+ }
+
+ return value;
+};
+
+const decodeRGB = int => ({
+ r: Math.max(0, (int >> 16)),
+ g: Math.max(0, (int >> 8) & 255),
+ b: Math.max(0, (int & 255)),
+});
+
+const luma = ({ r, g, b }) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
+
+const adjustColor = ({ r, g, b }, lumaThreshold = 100) => {
+ let delta;
+
+ if (luma({ r, g, b }) >= lumaThreshold) {
+ delta = -80;
+ } else {
+ delta = 80;
+ }
+
+ return {
+ r: r + delta,
+ g: g + delta,
+ b: b + delta,
+ };
+};
const messages = defineMessages({
play: { id: 'video.play', defaultMessage: 'Play' },
@@ -15,132 +140,168 @@ const messages = defineMessages({
download: { id: 'video.download', defaultMessage: 'Download file' },
});
+const TICK_SIZE = 10;
+const PADDING = 180;
+
export default @injectIntl
class Audio extends React.PureComponent {
static propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string,
+ poster: PropTypes.string,
duration: PropTypes.number,
- peaks: PropTypes.arrayOf(PropTypes.number),
+ width: PropTypes.number,
height: PropTypes.number,
- preload: PropTypes.bool,
editable: PropTypes.bool,
intl: PropTypes.object.isRequired,
+ cacheWidth: PropTypes.func,
};
state = {
+ width: this.props.width,
currentTime: 0,
+ buffer: 0,
duration: null,
paused: true,
muted: false,
volume: 0.5,
+ dragging: false,
+ color: { r: 255, g: 255, b: 255 },
};
- // Hard coded in components.scss
- // Any way to get ::before values programatically?
- volWidth = 50;
- volOffset = 70;
+ setPlayerRef = c => {
+ this.player = c;
- volHandleOffset = v => {
- const offset = v * this.volWidth + this.volOffset;
+ if (this.player) {
+ this._setDimensions();
+ }
+ }
- return (offset > 110) ? 110 : offset;
+ _setDimensions () {
+ const width = this.player.offsetWidth;
+ const height = width / (16/9);
+
+ if (this.props.cacheWidth) {
+ this.props.cacheWidth(width);
+ }
+
+ this.setState({ width, height });
+ }
+
+ setSeekRef = c => {
+ this.seek = c;
}
setVolumeRef = c => {
this.volume = c;
}
- setWaveformRef = c => {
- this.waveform = c;
+ setAudioRef = c => {
+ this.audio = c;
+
+ if (this.audio) {
+ this.setState({ volume: this.audio.volume, muted: this.audio.muted });
+ }
+ }
+
+ setBlurhashCanvasRef = c => {
+ this.blurhashCanvas = c;
+ }
+
+ setCanvasRef = c => {
+ this.canvas = c;
+
+ if (c) {
+ this.canvasContext = c.getContext('2d');
+ }
}
componentDidMount () {
- if (this.waveform) {
- this._updateWaveform();
- }
-
window.addEventListener('scroll', this.handleScroll);
+ window.addEventListener('resize', this.handleResize, { passive: true });
+
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+ img.onload = () => this.handlePosterLoad(img);
+ img.src = this.props.poster;
}
- componentDidUpdate (prevProps) {
- if (this.waveform && prevProps.src !== this.props.src) {
- this._updateWaveform();
+ componentDidUpdate (prevProps, prevState) {
+ if (prevProps.poster !== this.props.poster) {
+ const img = new Image();
+ img.crossOrigin = 'anonymous';
+ img.onload = () => this.handlePosterLoad(img);
+ img.src = this.props.poster;
}
+
+ if (prevState.blurhash !== this.state.blurhash) {
+ const context = this.blurhashCanvas.getContext('2d');
+ const pixels = decode(this.state.blurhash, 32, 32);
+ const outputImageData = new ImageData(pixels, 32, 32);
+
+ context.putImageData(outputImageData, 0, 0);
+ }
+
+ this._clear();
+ this._draw();
}
componentWillUnmount () {
window.removeEventListener('scroll', this.handleScroll);
-
- if (this.wavesurfer) {
- this.wavesurfer.destroy();
- this.wavesurfer = null;
- }
- }
-
- _updateWaveform () {
- const { src, height, duration, peaks, preload } = this.props;
-
- const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color');
- const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color');
-
- if (this.wavesurfer) {
- this.wavesurfer.destroy();
- this.loaded = false;
- }
-
- const wavesurfer = WaveSurfer.create({
- container: this.waveform,
- height,
- barWidth: 3,
- cursorWidth: 0,
- progressColor,
- waveColor,
- backend: 'MediaElement',
- interact: preload,
- });
-
- wavesurfer.setVolume(this.state.volume);
-
- if (preload) {
- wavesurfer.load(src);
- this.loaded = true;
- } else {
- wavesurfer.load(src, peaks, 'none', duration);
- this.loaded = false;
- }
-
- wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) }));
- wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) }));
- wavesurfer.on('pause', () => this.setState({ paused: true }));
- wavesurfer.on('play', () => this.setState({ paused: false }));
- wavesurfer.on('volume', volume => this.setState({ volume }));
- wavesurfer.on('mute', muted => this.setState({ muted }));
-
- this.wavesurfer = wavesurfer;
+ window.removeEventListener('resize', this.handleResize);
}
togglePlay = () => {
if (this.state.paused) {
- if (!this.props.preload && !this.loaded) {
- this.wavesurfer.createBackend();
- this.wavesurfer.createPeakCache();
- this.wavesurfer.load(this.props.src);
- this.wavesurfer.toggleInteraction();
- this.wavesurfer.setVolume(this.state.volume);
- this.loaded = true;
- }
-
- this.setState({ paused: false }, () => this.wavesurfer.play());
+ this.setState({ paused: false }, () => this.audio.play());
} else {
- this.setState({ paused: true }, () => this.wavesurfer.pause());
+ this.setState({ paused: true }, () => this.audio.pause());
+ }
+ }
+
+ handleResize = debounce(() => {
+ if (this.player) {
+ this._setDimensions();
+ }
+ }, 250, {
+ trailing: true,
+ });
+
+ handlePlay = () => {
+ this.setState({ paused: false });
+
+ if (this.canvas && !this.audioContext) {
+ this._initAudioContext();
+ }
+
+ if (this.audioContext && this.audioContext.state === 'suspended') {
+ this.audioContext.resume();
+ }
+
+ this._renderCanvas();
+ }
+
+ handlePause = () => {
+ this.setState({ paused: true });
+
+ if (this.audioContext) {
+ this.audioContext.suspend();
+ }
+ }
+
+ handleProgress = () => {
+ if (this.audio.buffered.length > 0) {
+ this.setState({ buffer: this.audio.buffered.end(0) / this.audio.duration * 100 });
}
}
toggleMute = () => {
const muted = !this.state.muted;
- this.setState({ muted }, () => this.wavesurfer.setMute(muted));
+
+ this.setState({ muted }, () => {
+ this.audio.muted = muted;
+ });
}
handleVolumeMouseDown = e => {
@@ -162,74 +323,374 @@ class Audio extends React.PureComponent {
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
}
+ handleMouseDown = e => {
+ document.addEventListener('mousemove', this.handleMouseMove, true);
+ document.addEventListener('mouseup', this.handleMouseUp, true);
+ document.addEventListener('touchmove', this.handleMouseMove, true);
+ document.addEventListener('touchend', this.handleMouseUp, true);
+
+ this.setState({ dragging: true });
+ this.audio.pause();
+ this.handleMouseMove(e);
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ handleMouseUp = () => {
+ document.removeEventListener('mousemove', this.handleMouseMove, true);
+ document.removeEventListener('mouseup', this.handleMouseUp, true);
+ document.removeEventListener('touchmove', this.handleMouseMove, true);
+ document.removeEventListener('touchend', this.handleMouseUp, true);
+
+ this.setState({ dragging: false });
+ this.audio.play();
+ }
+
+ handleMouseMove = throttle(e => {
+ const { x } = getPointerPosition(this.seek, e);
+ const currentTime = Math.floor(this.audio.duration * x);
+
+ if (!isNaN(currentTime)) {
+ this.setState({ currentTime }, () => {
+ this.audio.currentTime = currentTime;
+ });
+ }
+ }, 60);
+
+ handleTimeUpdate = () => {
+ this.setState({
+ currentTime: Math.floor(this.audio.currentTime),
+ duration: Math.floor(this.audio.duration),
+ });
+ }
+
handleMouseVolSlide = throttle(e => {
- const rect = this.volume.getBoundingClientRect();
- const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
+ const { x } = getPointerPosition(this.volume, e);
if(!isNaN(x)) {
- let slideamt = x;
-
- if (x > 1) {
- slideamt = 1;
- } else if(x < 0) {
- slideamt = 0;
- }
-
- this.wavesurfer.setVolume(slideamt);
+ this.setState({ volume: x }, () => {
+ this.audio.volume = x;
+ });
}
}, 60);
handleScroll = throttle(() => {
- if (!this.waveform || !this.wavesurfer) {
+ if (!this.canvas || !this.audio) {
return;
}
- const { top, height } = this.waveform.getBoundingClientRect();
+ const { top, height } = this.canvas.getBoundingClientRect();
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
if (!this.state.paused && !inView) {
- this.setState({ paused: true }, () => this.wavesurfer.pause());
+ this.setState({ paused: true }, () => this.audio.pause());
}
- }, 150, { trailing: true })
+ }, 150, { trailing: true });
+
+ handleMouseEnter = () => {
+ this.setState({ hovered: true });
+ }
+
+ handleMouseLeave = () => {
+ this.setState({ hovered: false });
+ }
+
+ _initAudioContext () {
+ const context = new AudioContext();
+ const analyser = context.createAnalyser();
+ const source = context.createMediaElementSource(this.audio);
+
+ analyser.smoothingTimeConstant = 0.6;
+ analyser.fftSize = 2048;
+
+ source.connect(analyser);
+ source.connect(context.destination);
+
+ this.audioContext = context;
+ this.analyser = analyser;
+ }
+
+ handlePosterLoad = image => {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+
+ canvas.width = image.width;
+ canvas.height = image.height;
+
+ context.drawImage(image, 0, 0);
+
+ const inputImageData = context.getImageData(0, 0, image.width, image.height);
+ const blurhash = encode(inputImageData.data, image.width, image.height, 4, 4);
+ const averageColor = decodeRGB(decode83(blurhash.slice(2, 6)));
+
+ this.setState({
+ blurhash,
+ color: adjustColor(averageColor),
+ darkText: luma(averageColor) >= 165,
+ });
+ }
+
+ handleDownload = () => {
+ fetch(this.props.src).then(res => res.blob()).then(blob => {
+ const element = document.createElement('a');
+ const objectURL = URL.createObjectURL(blob);
+
+ element.setAttribute('href', objectURL);
+ element.setAttribute('download', fileNameFromURL(this.props.src));
+
+ document.body.appendChild(element);
+ element.click();
+ document.body.removeChild(element);
+
+ URL.revokeObjectURL(objectURL);
+ }).catch(err => {
+ console.error(err);
+ });
+ }
+
+ _renderCanvas () {
+ requestAnimationFrame(() => {
+ this._clear();
+ this._draw();
+
+ if (!this.state.paused) {
+ this._renderCanvas();
+ }
+ });
+ }
+
+ _clear () {
+ this.canvasContext.clearRect(0, 0, this.state.width, this.state.height);
+ }
+
+ _draw () {
+ this.canvasContext.save();
+
+ const ticks = this._getTicks(360 * this._getScaleCoefficient(), TICK_SIZE);
+
+ ticks.forEach(tick => {
+ this._drawTick(tick.x1, tick.y1, tick.x2, tick.y2);
+ });
+
+ this.canvasContext.restore();
+ }
+
+ _getRadius () {
+ return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
+ }
+
+ _getScaleCoefficient () {
+ return (this.state.height || this.props.height) / 982;
+ }
+
+ _getTicks (count, size, animationParams = [0, 90]) {
+ const radius = this._getRadius();
+ const ticks = this._getTickPoints(count);
+ const lesser = 200;
+ const m = [];
+ const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0;
+ const frequencyData = new Uint8Array(bufferLength);
+ const allScales = [];
+ const scaleCoefficient = this._getScaleCoefficient();
+
+ if (this.analyser) {
+ this.analyser.getByteFrequencyData(frequencyData);
+ }
+
+ ticks.forEach((tick, i) => {
+ const coef = 1 - i / (ticks.length * 2.5);
+
+ let delta = ((frequencyData[i] || 0) - lesser * coef) * scaleCoefficient;
+
+ if (delta < 0) {
+ delta = 0;
+ }
+
+ let k;
+
+ if (animationParams[0] <= tick.angle && tick.angle <= animationParams[1]) {
+ k = radius / (radius - this._getSize(tick.angle, animationParams[0], animationParams[1]) - delta);
+ } else {
+ k = radius / (radius - (size + delta));
+ }
+
+ const x1 = tick.x * (radius - size);
+ const y1 = tick.y * (radius - size);
+ const x2 = x1 * k;
+ const y2 = y1 * k;
+
+ m.push({ x1, y1, x2, y2 });
+
+ if (i < 20) {
+ let scale = delta / (200 * scaleCoefficient);
+ scale = scale < 1 ? 1 : scale;
+ allScales.push(scale);
+ }
+ });
+
+ const scale = allScales.reduce((pv, cv) => pv + cv, 0) / allScales.length;
+
+ return m.map(({ x1, y1, x2, y2 }) => ({
+ x1: x1,
+ y1: y1,
+ x2: x2 * scale,
+ y2: y2 * scale,
+ }));
+ }
+
+ _getSize (angle, l, r) {
+ const scaleCoefficient = this._getScaleCoefficient();
+ const maxTickSize = TICK_SIZE * 9 * scaleCoefficient;
+ const m = (r - l) / 2;
+ const x = (angle - l);
+
+ let h;
+
+ if (x === m) {
+ return maxTickSize;
+ }
+
+ const d = Math.abs(m - x);
+ const v = 40 * Math.sqrt(1 / d);
+
+ if (v > maxTickSize) {
+ h = maxTickSize;
+ } else {
+ h = Math.max(TICK_SIZE, v);
+ }
+
+ return h;
+ }
+
+ _getTickPoints (count) {
+ const PI = 360;
+ const coords = [];
+ const step = PI / count;
+
+ let rad;
+
+ for(let deg = 0; deg < PI; deg += step) {
+ rad = deg * Math.PI / (PI / 2);
+ coords.push({ x: Math.cos(rad), y: -Math.sin(rad), angle: deg });
+ }
+
+ return coords;
+ }
+
+ _drawTick (x1, y1, x2, y2) {
+ const cx = this._getCX();
+ const cy = this._getCY();
+
+ const dx1 = Math.ceil(cx + x1);
+ const dy1 = Math.ceil(cy + y1);
+ const dx2 = Math.ceil(cx + x2);
+ const dy2 = Math.ceil(cy + y2);
+
+ const gradient = this.canvasContext.createLinearGradient(dx1, dy1, dx2, dy2);
+
+ const mainColor = `rgb(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b})`;
+ const lastColor = `rgba(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b}, 0)`;
+
+ gradient.addColorStop(0, mainColor);
+ gradient.addColorStop(0.6, mainColor);
+ gradient.addColorStop(1, lastColor);
+
+ this.canvasContext.beginPath();
+ this.canvasContext.strokeStyle = gradient;
+ this.canvasContext.lineWidth = 2;
+ this.canvasContext.moveTo(dx1, dy1);
+ this.canvasContext.lineTo(dx2, dy2);
+ this.canvasContext.stroke();
+ }
+
+ _getCX() {
+ return Math.floor(this.state.width / 2);
+ }
+
+ _getCY() {
+ return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
+ }
+
+ _getColor () {
+ return `rgb(${this.state.color.r}, ${this.state.color.g}, ${this.state.color.b})`;
+ }
render () {
- const { height, intl, alt, editable } = this.props;
- const { paused, muted, volume, currentTime } = this.state;
-
- const volumeWidth = muted ? 0 : volume * this.volWidth;
- const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
+ const { src, intl, alt, editable } = this.props;
+ const { paused, muted, volume, currentTime, duration, buffer, darkText, dragging } = this.state;
+ const progress = (currentTime / duration) * 100;
return (
-
-
-
+
+
-
+
+
+
+
+
+
-
-
-
+
-
+
{formatTime(currentTime)}
/
{formatTime(this.state.duration || Math.floor(this.props.duration))}
@@ -237,11 +698,7 @@ class Audio extends React.PureComponent {
diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
index a6186010b4..360a7af6ab 100644
--- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
@@ -203,7 +203,7 @@ class EmojiPickerMenu extends React.PureComponent {
if (!emoji.native) {
emoji.native = emoji.colons;
}
- if (!event.ctrlKey) {
+ if (!(event.ctrlKey || event.metaKey)) {
this.props.onClose();
}
this.props.onPick(emoji);
diff --git a/app/javascript/mastodon/features/status/components/card.js b/app/javascript/mastodon/features/status/components/card.js
index 630e99f2cb..4442ab4951 100644
--- a/app/javascript/mastodon/features/status/components/card.js
+++ b/app/javascript/mastodon/features/status/components/card.js
@@ -9,6 +9,7 @@ import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
import { useBlurhash } from 'mastodon/initial_state';
import { decode } from 'blurhash';
+import { debounce } from 'lodash';
const IDNA_PREFIX = 'xn--';
@@ -92,13 +93,20 @@ export default class Card extends React.PureComponent {
}
componentDidMount () {
+ window.addEventListener('resize', this.handleResize, { passive: true });
+
if (this.props.card && this.props.card.get('blurhash')) {
this._decode();
}
}
+ componentWillUnmount () {
+ window.removeEventListener('resize', this.handleResize);
+ }
+
componentDidUpdate (prevProps) {
const { card } = this.props;
+
if (card.get('blurhash') && (!prevProps.card || prevProps.card.get('blurhash') !== card.get('blurhash'))) {
this._decode();
}
@@ -118,6 +126,24 @@ export default class Card extends React.PureComponent {
}
}
+ _setDimensions () {
+ const width = this.node.offsetWidth;
+
+ if (this.props.cacheWidth) {
+ this.props.cacheWidth(width);
+ }
+
+ this.setState({ width });
+ }
+
+ handleResize = debounce(() => {
+ if (this.node) {
+ this._setDimensions();
+ }
+ }, 250, {
+ trailing: true,
+ });
+
handlePhotoClick = () => {
const { card, onOpenMedia } = this.props;
@@ -150,9 +176,10 @@ export default class Card extends React.PureComponent {
}
setRef = c => {
- if (c) {
- if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
- this.setState({ width: c.offsetWidth });
+ this.node = c;
+
+ if (this.node) {
+ this._setDimensions();
}
}
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 2ac47677ed..72d15ddf71 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -117,8 +117,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
src={attachment.get('url')}
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
- height={110}
- preload
+ poster={status.getIn(['account', 'avatar_static'])}
+ height={150}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js
index 9f6cbf988e..4ce4ac6c8c 100644
--- a/app/javascript/mastodon/features/ui/containers/status_list_container.js
+++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js
@@ -17,6 +17,8 @@ const makeGetStatusIds = (pending = false) => createSelector([
const statusForId = statuses.get(id);
let showStatus = true;
+ if (statusForId.get('account') === me) return true;
+
if (columnSettings.getIn(['shows', 'reblog']) === false) {
showStatus = showStatus && statusForId.get('reblog') === null;
}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 95e107618e..1f85375ffa 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { fromJS, is } from 'immutable';
-import { throttle } from 'lodash';
+import { throttle, debounce } from 'lodash';
import classNames from 'classnames';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
import { displayMedia, useBlurhash } from '../../initial_state';
@@ -19,7 +19,6 @@ const messages = defineMessages({
close: { id: 'video.close', defaultMessage: 'Close video' },
fullscreen: { id: 'video.fullscreen', defaultMessage: 'Full screen' },
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
- download: { id: 'video.download', defaultMessage: 'Download file' },
});
export const formatTime = secondsNum => {
@@ -87,6 +86,14 @@ export const getPointerPosition = (el, event) => {
return position;
};
+export const fileNameFromURL = str => {
+ const url = new URL(str);
+ const pathname = url.pathname;
+ const index = pathname.lastIndexOf('/');
+
+ return pathname.substring(index + 1);
+};
+
export default @injectIntl
class Video extends React.PureComponent {
@@ -126,29 +133,26 @@ class Video extends React.PureComponent {
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
};
- // Hard-coded in components.scss
- // Any way to get ::before values programatically?
- volWidth = 50;
- volOffset = 70;
-
- volHandleOffset = v => {
- const offset = v * this.volWidth + this.volOffset;
-
- return (offset > 110) ? 110 : offset;
- }
-
setPlayerRef = c => {
this.player = c;
- if (c) {
- if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
-
- this.setState({
- containerWidth: c.offsetWidth,
- });
+ if (this.player) {
+ this._setDimensions();
}
}
+ _setDimensions () {
+ const width = this.player.offsetWidth;
+
+ if (this.props.cacheWidth) {
+ this.props.cacheWidth(width);
+ }
+
+ this.setState({
+ containerWidth: width,
+ });
+ }
+
setVideoRef = c => {
this.video = c;
@@ -206,20 +210,12 @@ 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.
+ const { x } = getPointerPosition(this.volume, e);
if(!isNaN(x)) {
- let slideamt = x;
-
- if(x > 1) {
- slideamt = 1;
- } else if(x < 0) {
- slideamt = 0;
- }
-
- this.video.volume = slideamt;
- this.setState({ volume: slideamt });
+ this.setState({ volume: x }, () => {
+ this.video.volume = x;
+ });
}
}, 60);
@@ -280,6 +276,7 @@ class Video extends React.PureComponent {
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
window.addEventListener('scroll', this.handleScroll);
+ window.addEventListener('resize', this.handleResize, { passive: true });
if (this.props.blurhash) {
this._decode();
@@ -288,6 +285,7 @@ class Video extends React.PureComponent {
componentWillUnmount () {
window.removeEventListener('scroll', this.handleScroll);
+ window.removeEventListener('resize', this.handleResize);
document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
@@ -325,6 +323,14 @@ class Video extends React.PureComponent {
}
}
+ handleResize = debounce(() => {
+ if (this.player) {
+ this._setDimensions();
+ }
+ }, 250, {
+ trailing: true,
+ });
+
handleScroll = throttle(() => {
if (!this.video) {
return;
@@ -421,9 +427,6 @@ class Video extends React.PureComponent {
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = (currentTime / duration) * 100;
-
- const volumeWidth = (muted) ? 0 : volume * this.volWidth;
- const volumeHandleLoc = (muted) ? this.volHandleOffset(0) : this.volHandleOffset(volume);
const playerStyle = {};
let { width, height } = this.props;
@@ -510,18 +513,18 @@ class Video extends React.PureComponent {
-
-
-
+
{(detailed || fullscreen) && (
-
+
{formatTime(currentTime)}
/
{formatTime(duration)}
@@ -535,7 +538,6 @@ class Video extends React.PureComponent {
{(!onCloseVideo && !editable && !fullscreen) && }
{(!fullscreen && onOpenVideo) && }
{onCloseVideo && }
-
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index acbd21e8b2..6b47ce2118 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -5203,6 +5203,7 @@ a.status-card.compact:hover {
border-radius: 4px;
position: relative;
width: 100%;
+ min-height: 64px;
}
.media-gallery__item {
@@ -5296,6 +5297,7 @@ a.status-card.compact:hover {
}
.audio-player {
+ overflow: hidden;
box-sizing: border-box;
position: relative;
background: darken($ui-base-color, 8%);
@@ -5308,37 +5310,54 @@ a.status-card.compact:hover {
height: 100%;
}
- &__waveform {
- padding: 15px 0;
- position: relative;
- overflow: hidden;
+ .video-player__volume::before,
+ .video-player__seek::before {
+ background: rgba($white, 0.15);
+ }
- &::before {
- content: "";
- display: block;
- position: absolute;
- border-top: 1px solid lighten($ui-base-color, 4%);
- width: 100%;
- height: 0;
- left: 0;
- top: calc(50% + 1px);
+ &.with-light-background {
+ color: $black;
+
+ .video-player__volume::before,
+ .video-player__seek::before {
+ background: rgba($black, 0.15);
+ }
+
+ .video-player__seek__buffer {
+ background: rgba($black, 0.2);
+ }
+
+ .video-player__buttons button {
+ color: rgba($black, 0.75);
+
+ &:active,
+ &:hover,
+ &:focus {
+ color: $black;
+ }
+ }
+
+ .video-player__time-sep,
+ .video-player__time-total,
+ .video-player__time-current {
+ color: $black;
}
}
- &__progress-placeholder {
- background-color: rgba(lighten($ui-highlight-color, 8%), 0.5);
+ .video-player__seek::before,
+ .video-player__seek__buffer,
+ .video-player__seek__progress {
+ top: 0;
}
- &__wave-placeholder {
- background-color: lighten($ui-base-color, 16%);
+ .video-player__seek__handle {
+ top: -4px;
}
.video-player__controls {
padding: 0 15px;
padding-top: 10px;
- background: darken($ui-base-color, 8%);
- border-top: 1px solid lighten($ui-base-color, 4%);
- border-radius: 0 0 4px 4px;
+ background: transparent;
}
}
@@ -5350,6 +5369,7 @@ a.status-card.compact:hover {
border-radius: 4px;
box-sizing: border-box;
direction: ltr;
+ color: $white;
&.editable {
border-radius: 0;
@@ -5361,6 +5381,7 @@ a.status-card.compact:hover {
}
video {
+ display: block;
max-width: 100vw;
max-height: 80vh;
z-index: 1;
@@ -5461,6 +5482,10 @@ a.status-card.compact:hover {
}
&__buttons {
+ display: flex;
+ flex: 0 1 auto;
+ min-width: 30px;
+ align-items: center;
font-size: 16px;
white-space: nowrap;
overflow: hidden;
@@ -5479,6 +5504,7 @@ a.status-card.compact:hover {
}
button {
+ flex: 0 0 auto;
background: transparent;
padding: 2px 10px;
font-size: 16px;
@@ -5493,6 +5519,13 @@ a.status-card.compact:hover {
}
}
+ &__time {
+ display: inline;
+ flex: 0 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
&__time-sep,
&__time-total,
&__time-current {
@@ -5502,7 +5535,6 @@ a.status-card.compact:hover {
&__time-current {
color: $white;
- margin-left: 60px;
}
&__time-sep {
@@ -5516,9 +5548,22 @@ a.status-card.compact:hover {
}
&__volume {
+ flex: 0 0 auto;
+ display: inline-flex;
cursor: pointer;
height: 24px;
- display: inline;
+ position: relative;
+ overflow: hidden;
+
+ .no-reduce-motion & {
+ transition: all 100ms linear;
+ }
+
+ &.active {
+ overflow: visible;
+ width: 50px;
+ margin-right: 16px;
+ }
&::before {
content: "";
@@ -5528,8 +5573,9 @@ a.status-card.compact:hover {
display: block;
position: absolute;
height: 4px;
- left: 70px;
- bottom: 20px;
+ left: 0;
+ top: 50%;
+ transform: translate(0, -50%);
}
&__current {
@@ -5537,8 +5583,9 @@ a.status-card.compact:hover {
position: absolute;
height: 4px;
border-radius: 4px;
- left: 70px;
- bottom: 20px;
+ left: 0;
+ top: 50%;
+ transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
}
@@ -5548,12 +5595,21 @@ a.status-card.compact:hover {
border-radius: 50%;
width: 12px;
height: 12px;
- bottom: 16px;
- left: 70px;
- transition: opacity .1s ease;
+ top: 50%;
+ left: 0;
+ margin-left: -6px;
+ transform: translate(0, -50%);
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
- pointer-events: none;
+ opacity: 0;
+
+ .no-reduce-motion & {
+ transition: opacity 100ms linear;
+ }
+ }
+
+ &.active &__handle {
+ opacity: 1;
}
}
@@ -5613,10 +5669,12 @@ a.status-card.compact:hover {
height: 12px;
top: 6px;
margin-left: -6px;
- transition: opacity .1s ease;
background: lighten($ui-highlight-color, 8%);
box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2);
- pointer-events: none;
+
+ .no-reduce-motion & {
+ transition: opacity .1s ease;
+ }
&.active {
opacity: 1;
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 8b3198df7b..af0fa2b987 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -121,7 +121,7 @@ class FeedManager
crutches = build_crutches(into_account.id, statuses)
statuses.each do |status|
- next if filter_from_home?(status, into_account, crutches)
+ next if filter_from_home?(status, into_account.id, crutches)
add_to_feed(:home, into_account.id, status, aggregate)
end
diff --git a/db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb b/db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb
new file mode 100644
index 0000000000..c5688681fc
--- /dev/null
+++ b/db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb
@@ -0,0 +1,15 @@
+class AddFixedLowercaseIndexToAccounts < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ rename_index :accounts, 'index_accounts_on_username_and_domain_lower', 'old_index_accounts_on_username_and_domain_lower' unless index_name_exists?(:accounts, 'old_index_accounts_on_username_and_domain_lower')
+ add_index :accounts, "lower (username), COALESCE(lower(domain), '')", name: 'index_accounts_on_username_and_domain_lower', unique: true, algorithm: :concurrently
+ remove_index :accounts, name: 'old_index_accounts_on_username_and_domain_lower'
+ end
+
+ def down
+ add_index :accounts, 'lower (username), lower(domain)', name: 'old_index_accounts_on_username_and_domain_lower', unique: true, algorithm: :concurrently
+ remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower'
+ rename_index :accounts, 'old_index_accounts_on_username_and_domain_lower', 'index_accounts_on_username_and_domain_lower'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c1b6ffa815..2f3660dd5f 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: 2020_06_08_113046) do
+ActiveRecord::Schema.define(version: 2020_06_20_164023) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -176,7 +176,7 @@ ActiveRecord::Schema.define(version: 2020_06_08_113046) do
t.integer "header_storage_schema_version"
t.string "devices_url"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
- t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
+ t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
t.index ["uri"], name: "index_accounts_on_uri"
t.index ["url"], name: "index_accounts_on_url"
diff --git a/package.json b/package.json
index ba47aa0598..4a612fc200 100644
--- a/package.json
+++ b/package.json
@@ -154,7 +154,7 @@
"requestidlecallback": "^0.3.0",
"reselect": "^4.0.0",
"rimraf": "^3.0.2",
- "sass": "^1.26.5",
+ "sass": "^1.26.8",
"sass-loader": "^8.0.2",
"stacktrace-js": "^2.0.2",
"stringz": "^2.1.0",
diff --git a/yarn.lock b/yarn.lock
index e910604c6d..0370c6d29b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2727,9 +2727,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061:
- version "1.0.30001078"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001078.tgz#e1b6e2ae327b6a1ec11f65ec7a0dde1e7093074c"
- integrity sha512-sF12qXe9VMm32IEf/+NDvmTpwJaaU7N1igpiH2FdI4DyABJSsOqG3ZAcFvszLkoLoo1y6VJLMYivukUAxaMASw==
+ version "1.0.30001084"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001084.tgz#00e471931eaefbeef54f46aa2203914d3c165669"
+ integrity sha512-ftdc5oGmhEbLUuMZ/Qp3mOpzfZLCxPYKcvGv6v2dJJ+8EdqcvZRbAGOiLmkM/PV1QGta/uwBs8/nCl6sokDW6w==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3916,9 +3916,9 @@ ejs@^2.3.4, ejs@^2.6.1:
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
electron-to-chromium@^1.3.413:
- version "1.3.448"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.448.tgz#682831ecf3ce505231978f7c795a2813740cae7c"
- integrity sha512-WOr3SrZ55lUFYugA6sUu3H3ZoxVIH5o3zTSqYS+2DOJJP4hnHmBiD1w432a2YFW/H2G5FIxE6DB06rv+9dUL5g==
+ version "1.3.475"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.475.tgz#67688cc82c342f39594a412286e975eda45d8412"
+ integrity sha512-vcTeLpPm4+ccoYFXnepvkFt0KujdyrBU19KNEO40Pnkhta6mUi2K0Dn7NmpRcNz7BvysnSqeuIYScP003HWuYg==
elliptic@^6.0.0, elliptic@^6.5.2:
version "6.5.2"
@@ -4220,9 +4220,9 @@ escope@^3.6.0:
estraverse "^4.1.1"
eslint-import-resolver-node@^0.3.3:
- version "0.3.3"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz#dbaa52b6b2816b50bc6711af75422de808e98404"
- integrity sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
+ integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==
dependencies:
debug "^2.6.9"
resolve "^1.13.1"
@@ -4672,9 +4672,9 @@ fast-deep-equal@^3.1.1:
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
fast-glob@^3.1.1, fast-glob@^3.2.2:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d"
- integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A==
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
+ integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
@@ -7478,9 +7478,9 @@ natural-compare@^1.4.0:
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
nearley@^2.7.10:
- version "2.19.3"
- resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.3.tgz#ae3b040e27616b5348102c436d1719209476a5a1"
- integrity sha512-FpAy1PmTsUpOtgxr23g4jRNvJHYzZEW2PixXeSzksLR/ykPfwKhAodc2+9wQhY+JneWLcvkDw6q7FJIsIdF/aQ==
+ version "2.19.4"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.4.tgz#7518cbdd7d0e8e08b5f82841b9edb0126239c8b1"
+ integrity sha512-oqj3m4oqwKsN77pETa9IPvxHHHLW68KrDc2KYoWMUOhDlrNUo7finubwffQMBRnwNCOXc4kRxCZO0Rvx4L6Zrw==
dependencies:
commander "^2.19.0"
moo "^0.5.0"
@@ -9783,10 +9783,10 @@ sass-loader@^8.0.2:
schema-utils "^2.6.1"
semver "^6.3.0"
-sass@^1.26.5:
- version "1.26.5"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.5.tgz#2d7aecfbbabfa298567c8f06615b6e24d2d68099"
- integrity sha512-FG2swzaZUiX53YzZSjSakzvGtlds0lcbF+URuU9mxOv7WBh7NhXEVDa4kPKN4hN6fC2TkOTOKqiqp6d53N9X5Q==
+sass@^1.26.8:
+ version "1.26.8"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.8.tgz#312652530721f9568d4c4000b0db07ec6eb23325"
+ integrity sha512-yvtzyrKLGiXQu7H12ekXqsfoGT/aTKeMDyVzCB675k1HYuaj0py63i8Uf4SI9CHXj6apDhpfwbUr3gGOjdpu2Q==
dependencies:
chokidar ">=2.0.0 <4.0.0"