about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock8
-rw-r--r--app/javascript/flavours/glitch/components/dropdown_menu.js29
-rw-r--r--app/javascript/flavours/glitch/components/status.js6
-rw-r--r--app/javascript/flavours/glitch/containers/status_container.js4
-rw-r--r--app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js35
-rw-r--r--app/javascript/flavours/glitch/features/status/components/detailed_status.js4
-rw-r--r--app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js4
-rw-r--r--app/javascript/flavours/glitch/features/status/index.js6
-rw-r--r--app/javascript/flavours/glitch/features/ui/components/video_modal.js13
-rw-r--r--app/javascript/flavours/glitch/features/video/index.js17
-rw-r--r--app/javascript/flavours/glitch/styles/components/media.scss7
-rw-r--r--app/javascript/flavours/glitch/util/resize_image.js4
-rw-r--r--app/javascript/mastodon/components/dropdown_menu.js29
-rw-r--r--app/javascript/mastodon/components/status.js6
-rw-r--r--app/javascript/mastodon/containers/status_container.js4
-rw-r--r--app/javascript/mastodon/features/compose/components/privacy_dropdown.js35
-rw-r--r--app/javascript/mastodon/features/status/components/detailed_status.js4
-rw-r--r--app/javascript/mastodon/features/status/containers/detailed_status_container.js4
-rw-r--r--app/javascript/mastodon/features/status/index.js6
-rw-r--r--app/javascript/mastodon/features/ui/components/video_modal.js13
-rw-r--r--app/javascript/mastodon/features/video/index.js17
-rw-r--r--app/javascript/mastodon/utils/resize_image.js4
-rw-r--r--app/javascript/styles/mastodon/components.scss7
-rw-r--r--app/models/relay.rb4
-rw-r--r--package.json2
-rw-r--r--yarn.lock37
26 files changed, 155 insertions, 154 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index b70dd2502..13f6bed59 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -407,7 +407,7 @@ GEM
     parallel (1.19.1)
     parallel_tests (2.32.0)
       parallel
-    parser (2.7.1.0)
+    parser (2.7.1.1)
       ast (~> 2.4.0)
     parslet (1.8.2)
     pastel (0.7.3)
@@ -492,9 +492,9 @@ GEM
       rdf (~> 3.1)
     redcarpet (3.5.0)
     redis (4.1.3)
-    redis-actionpack (5.0.2)
-      actionpack (>= 4.0, < 6)
-      redis-rack (>= 1, < 3)
+    redis-actionpack (5.2.0)
+      actionpack (>= 5, < 7)
+      redis-rack (>= 2.1.0, < 3)
       redis-store (>= 1.1.0, < 2)
     redis-activesupport (5.0.4)
       activesupport (>= 3, < 6)
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js
index cc4d714e8..b3da4fc2f 100644
--- a/app/javascript/flavours/glitch/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/components/dropdown_menu.js
@@ -68,20 +68,14 @@ class DropdownMenu extends React.PureComponent {
   handleKeyDown = e => {
     const items = Array.from(this.node.getElementsByTagName('a'));
     const index = items.indexOf(document.activeElement);
-    let element;
+    let element = null;
 
     switch(e.key) {
     case 'ArrowDown':
-      element = items[index+1];
-      if (element) {
-        element.focus();
-      }
+      element = items[index+1] || items[0];
       break;
     case 'ArrowUp':
-      element = items[index-1];
-      if (element) {
-        element.focus();
-      }
+      element = items[index-1] || items[items.length-1];
       break;
     case 'Tab':
       if (e.shiftKey) {
@@ -89,28 +83,23 @@ class DropdownMenu extends React.PureComponent {
       } else {
         element = items[index+1] || items[0];
       }
-      if (element) {
-        element.focus();
-        e.preventDefault();
-        e.stopPropagation();
-      }
       break;
     case 'Home':
       element = items[0];
-      if (element) {
-        element.focus();
-      }
       break;
     case 'End':
       element = items[items.length-1];
-      if (element) {
-        element.focus();
-      }
       break;
     case 'Escape':
       this.props.onClose();
       break;
     }
+
+    if (element) {
+      element.focus();
+      e.preventDefault();
+      e.stopPropagation();
+    }
   }
 
   handleItemKeyPress = e => {
diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js
index 23cdc0167..91bc06b3c 100644
--- a/app/javascript/flavours/glitch/components/status.js
+++ b/app/javascript/flavours/glitch/components/status.js
@@ -372,8 +372,8 @@ class Status extends ImmutablePureComponent {
     }
   };
 
-  handleOpenVideo = (media, startTime) => {
-    this.props.onOpenVideo(media, startTime);
+  handleOpenVideo = (media, options) => {
+    this.props.onOpenVideo(media, options);
   }
 
   handleHotkeyOpenMedia = e => {
@@ -385,7 +385,7 @@ class Status extends ImmutablePureComponent {
       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
         // TODO: toggle play/paused?
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        onOpenVideo(status.getIn(['media_attachments', 0]), 0);
+        onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
       } else {
         onOpenMedia(status.get('media_attachments'), 0);
       }
diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js
index f6e92813a..2cbe3d094 100644
--- a/app/javascript/flavours/glitch/containers/status_container.js
+++ b/app/javascript/flavours/glitch/containers/status_container.js
@@ -178,8 +178,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
     dispatch(openModal('MEDIA', { media, index }));
   },
 
-  onOpenVideo (media, time) {
-    dispatch(openModal('VIDEO', { media, time }));
+  onOpenVideo (media, options) {
+    dispatch(openModal('VIDEO', { media, options }));
   },
 
   onBlock (status) {
diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
index 404504e84..84c040a86 100644
--- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
+++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js
@@ -106,7 +106,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
     const index = items.findIndex(item => {
       return (item.name === name);
     });
-    let element;
+    let element = null;
 
     switch(e.key) {
     case 'Escape':
@@ -117,18 +117,10 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
       this.handleClick(e);
       break;
     case 'ArrowDown':
-      element = this.node.childNodes[index + 1];
-      if (element) {
-        element.focus();
-        this.handleChange(element.getAttribute('data-index'));
-      }
+      element = this.node.childNodes[index + 1] || this.node.firstChild;
       break;
     case 'ArrowUp':
-      element = this.node.childNodes[index - 1];
-      if (element) {
-        element.focus();
-        this.handleChange(element.getAttribute('data-index'));
-      }
+      element = this.node.childNodes[index - 1] || this.node.lastChild;
       break;
     case 'Tab':
       if (e.shiftKey) {
@@ -136,28 +128,21 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent
       } else {
         element = this.node.childNodes[index + 1] || this.node.firstChild;
       }
-      if (element) {
-        element.focus();
-        this.handleChange(element.getAttribute('data-index'));
-        e.preventDefault();
-        e.stopPropagation();
-      }
       break;
     case 'Home':
       element = this.node.firstChild;
-      if (element) {
-        element.focus();
-        this.handleChange(element.getAttribute('data-index'));
-      }
       break;
     case 'End':
       element = this.node.lastChild;
-      if (element) {
-        element.focus();
-        this.handleChange(element.getAttribute('data-index'));
-      }
       break;
     }
+
+    if (element) {
+      element.focus();
+      this.handleChange(element.getAttribute('data-index'));
+      e.preventDefault();
+      e.stopPropagation();
+    }
   }
 
   setFocusRef = c => {
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 180b11a54..17f22a8a2 100644
--- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js
+++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js
@@ -66,8 +66,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
     e.stopPropagation();
   }
 
-  handleOpenVideo = (media, startTime) => {
-    this.props.onOpenVideo(media, startTime);
+  handleOpenVideo = (media, options) => {
+    this.props.onOpenVideo(media, options);
   }
 
   _measureHeight (heightJustChanged) {
diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
index e71803328..9d11f37e0 100644
--- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
+++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js
@@ -130,8 +130,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(openModal('MEDIA', { media, index }));
   },
 
-  onOpenVideo (media, time) {
-    dispatch(openModal('VIDEO', { media, time }));
+  onOpenVideo (media, options) {
+    dispatch(openModal('VIDEO', { media, options }));
   },
 
   onBlock (status) {
diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js
index 411d2a88d..a9abc545e 100644
--- a/app/javascript/flavours/glitch/features/status/index.js
+++ b/app/javascript/flavours/glitch/features/status/index.js
@@ -316,8 +316,8 @@ class Status extends ImmutablePureComponent {
     this.props.dispatch(openModal('MEDIA', { media, index }));
   }
 
-  handleOpenVideo = (media, time) => {
-    this.props.dispatch(openModal('VIDEO', { media, time }));
+  handleOpenVideo = (media, options) => {
+    this.props.dispatch(openModal('VIDEO', { media, options }));
   }
 
   handleHotkeyOpenMedia = e => {
@@ -329,7 +329,7 @@ class Status extends ImmutablePureComponent {
       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
         // TODO: toggle play/paused?
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
+        this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
       } else {
         this.handleOpenMedia(status.get('media_attachments'), 0);
       }
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 e7309021e..afeff90a4 100644
--- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js
+++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js
@@ -16,7 +16,11 @@ export default class VideoModal extends ImmutablePureComponent {
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     status: ImmutablePropTypes.map,
-    time: PropTypes.number,
+    options: PropTypes.shape({
+      startTime: PropTypes.number,
+      autoPlay: PropTypes.bool,
+      defaultVolume: PropTypes.number,
+    }),
     onClose: PropTypes.func.isRequired,
   };
 
@@ -28,7 +32,8 @@ export default class VideoModal extends ImmutablePureComponent {
   }
 
   render () {
-    const { media, status, time, onClose } = this.props;
+    const { media, status, onClose } = this.props;
+    const options = this.props.options || {};
 
     return (
       <div className='modal-root__modal video-modal'>
@@ -37,7 +42,9 @@ export default class VideoModal extends ImmutablePureComponent {
             preview={media.get('preview_url')}
             blurhash={media.get('blurhash')}
             src={media.get('url')}
-            startTime={time}
+            startTime={options.startTime}
+            autoPlay={options.autoPlay}
+            defaultVolume={options.defaultVolume}
             onCloseVideo={onClose}
             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 6a8952c8d..a89d9c8b0 100644
--- a/app/javascript/flavours/glitch/features/video/index.js
+++ b/app/javascript/flavours/glitch/features/video/index.js
@@ -111,6 +111,8 @@ class Video extends React.PureComponent {
     preventPlayback: PropTypes.bool,
     blurhash: PropTypes.string,
     link: PropTypes.node,
+    autoPlay: PropTypes.bool,
+    defaultVolume: PropTypes.number,
   };
 
   state = {
@@ -360,6 +362,13 @@ class Video extends React.PureComponent {
   handleLoadedData = () => {
     if (this.props.startTime) {
       this.video.currentTime = this.props.startTime;
+    }
+
+    if (this.props.defaultVolume !== undefined) {
+      this.video.volume = this.props.defaultVolume;
+    }
+
+    if (this.props.autoPlay) {
       this.video.play();
     }
   }
@@ -386,8 +395,14 @@ class Video extends React.PureComponent {
       height,
     });
 
+    const options = {
+      startTime: this.video.currentTime,
+      autoPlay: !this.state.paused,
+      defaultVolume: this.state.volume,
+    };
+
     this.video.pause();
-    this.props.onOpenVideo(media, this.video.currentTime);
+    this.props.onOpenVideo(media, options);
   }
 
   handleCloseVideo = () => {
diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss
index 3cb076191..dbf0c908d 100644
--- a/app/javascript/flavours/glitch/styles/components/media.scss
+++ b/app/javascript/flavours/glitch/styles/components/media.scss
@@ -691,3 +691,10 @@
     }
   }
 }
+
+.gifv {
+  video {
+    max-width: 100vw;
+    max-height: 80vh;
+  }
+}
diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js
index 61f924f70..8c89b2841 100644
--- a/app/javascript/flavours/glitch/util/resize_image.js
+++ b/app/javascript/flavours/glitch/util/resize_image.js
@@ -138,7 +138,7 @@ const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) =
     .catch(reject);
 });
 
-export default inputFile => new Promise((resolve, reject) => {
+export default inputFile => new Promise((resolve) => {
   if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
     resolve(inputFile);
     return;
@@ -153,5 +153,5 @@ export default inputFile => new Promise((resolve, reject) => {
     resizeImage(img, inputFile.type)
       .then(resolve)
       .catch(() => resolve(inputFile));
-  }).catch(reject);
+  }).catch(() => resolve(inputFile));
 });
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index a4f262285..31c02d735 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -68,20 +68,14 @@ class DropdownMenu extends React.PureComponent {
   handleKeyDown = e => {
     const items = Array.from(this.node.getElementsByTagName('a'));
     const index = items.indexOf(document.activeElement);
-    let element;
+    let element = null;
 
     switch(e.key) {
     case 'ArrowDown':
-      element = items[index+1];
-      if (element) {
-        element.focus();
-      }
+      element = items[index+1] || items[0];
       break;
     case 'ArrowUp':
-      element = items[index-1];
-      if (element) {
-        element.focus();
-      }
+      element = items[index-1] || items[items.length-1];
       break;
     case 'Tab':
       if (e.shiftKey) {
@@ -89,28 +83,23 @@ class DropdownMenu extends React.PureComponent {
       } else {
         element = items[index+1] || items[0];
       }
-      if (element) {
-        element.focus();
-        e.preventDefault();
-        e.stopPropagation();
-      }
       break;
     case 'Home':
       element = items[0];
-      if (element) {
-        element.focus();
-      }
       break;
     case 'End':
       element = items[items.length-1];
-      if (element) {
-        element.focus();
-      }
       break;
     case 'Escape':
       this.props.onClose();
       break;
     }
+
+    if (element) {
+      element.focus();
+      e.preventDefault();
+      e.stopPropagation();
+    }
   }
 
   handleItemKeyPress = e => {
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js
index 075ee1b87..9e4442cef 100644
--- a/app/javascript/mastodon/components/status.js
+++ b/app/javascript/mastodon/components/status.js
@@ -176,8 +176,8 @@ class Status extends ImmutablePureComponent {
     return <div className='audio-player' style={{ height: '110px' }} />;
   }
 
-  handleOpenVideo = (media, startTime) => {
-    this.props.onOpenVideo(media, startTime);
+  handleOpenVideo = (media, options) => {
+    this.props.onOpenVideo(media, options);
   }
 
   handleHotkeyOpenMedia = e => {
@@ -190,7 +190,7 @@ class Status extends ImmutablePureComponent {
       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
         // TODO: toggle play/paused?
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        onOpenVideo(status.getIn(['media_attachments', 0]), 0);
+        onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
       } else {
         onOpenMedia(status.get('media_attachments'), 0);
       }
diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js
index 2ba3a3123..decf7279f 100644
--- a/app/javascript/mastodon/containers/status_container.js
+++ b/app/javascript/mastodon/containers/status_container.js
@@ -150,8 +150,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(openModal('MEDIA', { media, index }));
   },
 
-  onOpenVideo (media, time) {
-    dispatch(openModal('VIDEO', { media, time }));
+  onOpenVideo (media, options) {
+    dispatch(openModal('VIDEO', { media, options }));
   },
 
   onBlock (status) {
diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
index de030b7a2..57588fe96 100644
--- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
+++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js
@@ -50,7 +50,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
     const index = items.findIndex(item => {
       return (item.value === value);
     });
-    let element;
+    let element = null;
 
     switch(e.key) {
     case 'Escape':
@@ -60,18 +60,10 @@ class PrivacyDropdownMenu extends React.PureComponent {
       this.handleClick(e);
       break;
     case 'ArrowDown':
-      element = this.node.childNodes[index + 1];
-      if (element) {
-        element.focus();
-        this.props.onChange(element.getAttribute('data-index'));
-      }
+      element = this.node.childNodes[index + 1] || this.node.firstChild;
       break;
     case 'ArrowUp':
-      element = this.node.childNodes[index - 1];
-      if (element) {
-        element.focus();
-        this.props.onChange(element.getAttribute('data-index'));
-      }
+      element = this.node.childNodes[index - 1] || this.node.lastChild;
       break;
     case 'Tab':
       if (e.shiftKey) {
@@ -79,28 +71,21 @@ class PrivacyDropdownMenu extends React.PureComponent {
       } else {
         element = this.node.childNodes[index + 1] || this.node.firstChild;
       }
-      if (element) {
-        element.focus();
-        this.props.onChange(element.getAttribute('data-index'));
-        e.preventDefault();
-        e.stopPropagation();
-      }
       break;
     case 'Home':
       element = this.node.firstChild;
-      if (element) {
-        element.focus();
-        this.props.onChange(element.getAttribute('data-index'));
-      }
       break;
     case 'End':
       element = this.node.lastChild;
-      if (element) {
-        element.focus();
-        this.props.onChange(element.getAttribute('data-index'));
-      }
       break;
     }
+
+    if (element) {
+      element.focus();
+      this.props.onChange(element.getAttribute('data-index'));
+      e.preventDefault();
+      e.stopPropagation();
+    }
   }
 
   handleClick = e => {
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 7a82fa13a..4201b237e 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -48,8 +48,8 @@ export default class DetailedStatus extends ImmutablePureComponent {
     e.stopPropagation();
   }
 
-  handleOpenVideo = (media, startTime) => {
-    this.props.onOpenVideo(media, startTime);
+  handleOpenVideo = (media, options) => {
+    this.props.onOpenVideo(media, options);
   }
 
   handleExpandedToggle = () => {
diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
index 333c295dc..6d5c33240 100644
--- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js
+++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js
@@ -129,8 +129,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
     dispatch(openModal('MEDIA', { media, index }));
   },
 
-  onOpenVideo (media, time) {
-    dispatch(openModal('VIDEO', { media, time }));
+  onOpenVideo (media, options) {
+    dispatch(openModal('VIDEO', { media, options }));
   },
 
   onBlock (status) {
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index c058120d6..179df53a1 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -277,8 +277,8 @@ class Status extends ImmutablePureComponent {
     this.props.dispatch(openModal('MEDIA', { media, index }));
   }
 
-  handleOpenVideo = (media, time) => {
-    this.props.dispatch(openModal('VIDEO', { media, time }));
+  handleOpenVideo = (media, options) => {
+    this.props.dispatch(openModal('VIDEO', { media, options }));
   }
 
   handleHotkeyOpenMedia = e => {
@@ -290,7 +290,7 @@ class Status extends ImmutablePureComponent {
       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
         // TODO: toggle play/paused?
       } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
-        this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
+        this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
       } else {
         this.handleOpenMedia(status.get('media_attachments'), 0);
       }
diff --git a/app/javascript/mastodon/features/ui/components/video_modal.js b/app/javascript/mastodon/features/ui/components/video_modal.js
index f37fc796f..e28bd5b49 100644
--- a/app/javascript/mastodon/features/ui/components/video_modal.js
+++ b/app/javascript/mastodon/features/ui/components/video_modal.js
@@ -14,7 +14,11 @@ export default class VideoModal extends ImmutablePureComponent {
   static propTypes = {
     media: ImmutablePropTypes.map.isRequired,
     status: ImmutablePropTypes.map,
-    time: PropTypes.number,
+    options: PropTypes.shape({
+      startTime: PropTypes.number,
+      autoPlay: PropTypes.bool,
+      defaultVolume: PropTypes.number,
+    }),
     onClose: PropTypes.func.isRequired,
   };
 
@@ -52,7 +56,8 @@ export default class VideoModal extends ImmutablePureComponent {
   }
 
   render () {
-    const { media, status, time, onClose } = this.props;
+    const { media, status, onClose } = this.props;
+    const options = this.props.options || {};
 
     return (
       <div className='modal-root__modal video-modal'>
@@ -61,7 +66,9 @@ export default class VideoModal extends ImmutablePureComponent {
             preview={media.get('preview_url')}
             blurhash={media.get('blurhash')}
             src={media.get('url')}
-            startTime={time}
+            startTime={options.startTime}
+            autoPlay={options.autoPlay}
+            defaultVolume={options.defaultVolume}
             onCloseVideo={onClose}
             detailed
             alt={media.get('description')}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index 42ded9d21..95e107618 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -109,6 +109,8 @@ class Video extends React.PureComponent {
     intl: PropTypes.object.isRequired,
     blurhash: PropTypes.string,
     link: PropTypes.node,
+    autoPlay: PropTypes.bool,
+    defaultVolume: PropTypes.number,
   };
 
   state = {
@@ -367,6 +369,13 @@ class Video extends React.PureComponent {
   handleLoadedData = () => {
     if (this.props.startTime) {
       this.video.currentTime = this.props.startTime;
+    }
+
+    if (this.props.defaultVolume !== undefined) {
+      this.video.volume = this.props.defaultVolume;
+    }
+
+    if (this.props.autoPlay) {
       this.video.play();
     }
   }
@@ -393,8 +402,14 @@ class Video extends React.PureComponent {
       height,
     });
 
+    const options = {
+      startTime: this.video.currentTime,
+      autoPlay: !this.state.paused,
+      defaultVolume: this.state.volume,
+    };
+
     this.video.pause();
-    this.props.onOpenVideo(media, this.video.currentTime);
+    this.props.onOpenVideo(media, options);
   }
 
   handleCloseVideo = () => {
diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js
index 710a08f7c..6c1cb61a2 100644
--- a/app/javascript/mastodon/utils/resize_image.js
+++ b/app/javascript/mastodon/utils/resize_image.js
@@ -138,7 +138,7 @@ const resizeImage = (img, type = 'image/png') => new Promise((resolve, reject) =
     .catch(reject);
 });
 
-export default inputFile => new Promise((resolve, reject) => {
+export default inputFile => new Promise((resolve) => {
   if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
     resolve(inputFile);
     return;
@@ -153,5 +153,5 @@ export default inputFile => new Promise((resolve, reject) => {
     resizeImage(img, inputFile.type)
       .then(resolve)
       .catch(() => resolve(inputFile));
-  }).catch(reject);
+  }).catch(() => resolve(inputFile));
 });
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index dd82b0824..c7835b878 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -5575,6 +5575,13 @@ a.status-card.compact:hover {
   }
 }
 
+.gifv {
+  video {
+    max-width: 100vw;
+    max-height: 80vh;
+  }
+}
+
 .directory {
   &__list {
     width: 100%;
diff --git a/app/models/relay.rb b/app/models/relay.rb
index 870f31979..d6ddd30ed 100644
--- a/app/models/relay.rb
+++ b/app/models/relay.rb
@@ -27,7 +27,7 @@ class Relay < ApplicationRecord
     payload     = Oj.dump(follow_activity(activity_id))
 
     update!(state: :pending, follow_activity_id: activity_id)
-    DeliveryFailureTracker.track_success!(inbox_url)
+    DeliveryFailureTracker.reset!(inbox_url)
     ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
   end
 
@@ -36,7 +36,7 @@ class Relay < ApplicationRecord
     payload     = Oj.dump(unfollow_activity(activity_id))
 
     update!(state: :idle, follow_activity_id: nil)
-    DeliveryFailureTracker.track_success!(inbox_url)
+    DeliveryFailureTracker.reset!(inbox_url)
     ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
   end
 
diff --git a/package.json b/package.json
index 684dff375..6b52da1e9 100644
--- a/package.json
+++ b/package.json
@@ -163,7 +163,7 @@
     "tesseract.js": "^2.0.0-alpha.16",
     "throng": "^4.0.0",
     "tiny-queue": "^0.2.1",
-    "uuid": "^7.0.2",
+    "uuid": "^7.0.3",
     "wavesurfer.js": "^3.3.1",
     "webpack": "^4.42.1",
     "webpack-assets-manifest": "^3.1.1",
diff --git a/yarn.lock b/yarn.lock
index 3fc6a6f46..2d57ec0ba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5065,12 +5065,12 @@ globby@^7.1.1:
     slash "^1.0.0"
 
 globule@^1.0.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d"
-  integrity sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.1.tgz#90a25338f22b7fbeb527cee63c629aea754d33b9"
+  integrity sha512-OVyWOHgw29yosRHCHo7NncwR1hW5ew0W/UrvtwvjefVJeQ26q4/8r8FmPsSF1hJ93IgWkyv16pCTz6WblMzm/g==
   dependencies:
     glob "~7.1.1"
-    lodash "~4.17.10"
+    lodash "~4.17.12"
     minimatch "~3.0.2"
 
 gonzales-pe-sl@^4.2.3:
@@ -6922,7 +6922,7 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@~4.17.10:
+lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.3.0, lodash@~4.17.12:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -10023,16 +10023,11 @@ side-channel@^1.0.2:
     es-abstract "^1.17.0-next.1"
     object-inspect "^1.7.0"
 
-signal-exit@^3.0.0:
+signal-exit@^3.0.0, signal-exit@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
-signal-exit@^3.0.2:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
-  integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
-
 simple-swizzle@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@@ -10041,9 +10036,9 @@ simple-swizzle@^0.2.2:
     is-arrayish "^0.3.1"
 
 sisteransi@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.0.tgz#77d9622ff909080f1c19e5f4a1df0c1b0a27b88c"
-  integrity sha512-N+z4pHB4AmUv0SjveWRd6q1Nj5w62m5jodv+GD8lvmbY/83T/rpbJGZOnK5T149OldDj4Db07BSv9xY4K6NTPQ==
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
+  integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
 
 slash@^1.0.0:
   version "1.0.0"
@@ -10366,9 +10361,9 @@ stream-http@^2.7.2:
     xtend "^4.0.0"
 
 stream-shift@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
-  integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
+  integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
 strict-uri-encode@^1.0.0:
   version "1.1.0"
@@ -11147,10 +11142,10 @@ uuid@^3.0.1, uuid@^3.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
-uuid@^7.0.2:
-  version "7.0.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6"
-  integrity sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==
+uuid@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
+  integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
 
 v8-compile-cache@2.0.3, v8-compile-cache@^2.0.3:
   version "2.0.3"