From d3cd37d73e579f62901f6c09b53073f490f4caf3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Marques?=
<64037198+TheDevJoao@users.noreply.github.com>
Date: Tue, 7 Nov 2023 20:37:58 -0300
Subject: [PATCH] Feature - Prevents multiple audio/video attachments from
being played at the same time (#24717)
---
.../features/__tests__/toggle-play.jsx | 80 +++++++++++++++++++
.../mastodon/features/audio/index.jsx | 33 ++++++--
.../mastodon/features/video/index.jsx | 31 ++++++-
.../mastodon/reducers/media_attachments.js | 7 ++
4 files changed, 140 insertions(+), 11 deletions(-)
create mode 100644 app/javascript/mastodon/features/__tests__/toggle-play.jsx
diff --git a/app/javascript/mastodon/features/__tests__/toggle-play.jsx b/app/javascript/mastodon/features/__tests__/toggle-play.jsx
new file mode 100644
index 0000000000..9c999db867
--- /dev/null
+++ b/app/javascript/mastodon/features/__tests__/toggle-play.jsx
@@ -0,0 +1,80 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+
+import { render, fireEvent } from '@testing-library/react';
+
+class Media extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ paused: props.paused || false,
+ };
+ }
+
+ handleMediaClick = () => {
+ const { onClick } = this.props;
+
+ this.setState(prevState => ({
+ paused: !prevState.paused,
+ }));
+
+ if (typeof onClick === 'function') {
+ onClick();
+ }
+
+ const { title } = this.props;
+ const mediaElements = document.querySelectorAll(`div[title="${title}"]`);
+
+ setTimeout(() => {
+ mediaElements.forEach(element => {
+ if (element !== this && !element.classList.contains('paused')) {
+ element.click();
+ }
+ });
+ }, 0);
+ };
+
+ render() {
+ const { title } = this.props;
+ const { paused } = this.state;
+
+ return (
+
+ );
+ }
+
+}
+
+Media.propTypes = {
+ title: PropTypes.string.isRequired,
+ onClick: PropTypes.func,
+ paused: PropTypes.bool,
+};
+
+describe('Media attachments test', () => {
+ let currentMedia = null;
+ const togglePlayMock = jest.fn();
+
+ it('plays a new media file and pauses others that were playing', () => {
+ const container = render(
+
+
+
+
,
+ );
+
+ fireEvent.click(container.getByTitle('firstMedia'));
+ expect(togglePlayMock).toHaveBeenCalledTimes(1);
+ currentMedia = container.getByTitle('firstMedia');
+ expect(currentMedia.textContent).toMatch(/Playing/);
+
+ fireEvent.click(container.getByTitle('secondMedia'));
+ expect(togglePlayMock).toHaveBeenCalledTimes(2);
+ currentMedia = container.getByTitle('secondMedia');
+ expect(currentMedia.textContent).toMatch(/Playing/);
+ });
+});
diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx
index 7a7d0910fa..fac43416c9 100644
--- a/app/javascript/mastodon/features/audio/index.jsx
+++ b/app/javascript/mastodon/features/audio/index.jsx
@@ -20,6 +20,7 @@ import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/featur
import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state';
+import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
import Visualizer from './visualizer';
@@ -165,15 +166,32 @@ class Audio extends PureComponent {
}
togglePlay = () => {
- if (!this.audioContext) {
- this._initAudioContext();
+ const audios = document.querySelectorAll('audio');
+
+ audios.forEach((audio) => {
+ const button = audio.previousElementSibling;
+ button.addEventListener('click', () => {
+ if(audio.paused) {
+ audios.forEach((e) => {
+ if (e !== audio) {
+ e.pause();
+ }
+ });
+ audio.play();
+ this.setState({ paused: false });
+ } else {
+ audio.pause();
+ this.setState({ paused: true });
+ }
+ });
+ });
+
+ if (currentMedia !== null) {
+ currentMedia.pause();
}
- if (this.state.paused) {
- this.setState({ paused: false }, () => this.audio.play());
- } else {
- this.setState({ paused: true }, () => this.audio.pause());
- }
+ this.audio.play();
+ setCurrentMedia(this.audio);
};
handleResize = debounce(() => {
@@ -195,6 +213,7 @@ class Audio extends PureComponent {
};
handlePause = () => {
+ this.audio.pause();
this.setState({ paused: true });
if (this.audioContext) {
diff --git a/app/javascript/mastodon/features/video/index.jsx b/app/javascript/mastodon/features/video/index.jsx
index 0097537d57..f88e9042ef 100644
--- a/app/javascript/mastodon/features/video/index.jsx
+++ b/app/javascript/mastodon/features/video/index.jsx
@@ -22,6 +22,7 @@ import { Icon } from 'mastodon/components/icon';
import { playerSettings } from 'mastodon/settings';
import { displayMedia, useBlurhash } from '../../initial_state';
+import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
const messages = defineMessages({
@@ -181,6 +182,7 @@ class Video extends PureComponent {
};
handlePause = () => {
+ this.video.pause();
this.setState({ paused: true });
};
@@ -344,11 +346,32 @@ class Video extends PureComponent {
};
togglePlay = () => {
- if (this.state.paused) {
- this.setState({ paused: false }, () => this.video.play());
- } else {
- this.setState({ paused: true }, () => this.video.pause());
+ const videos = document.querySelectorAll('video');
+
+ videos.forEach((video) => {
+ const button = video.nextElementSibling;
+ button.addEventListener('click', () => {
+ if (video.paused) {
+ videos.forEach((e) => {
+ if (e !== video) {
+ e.pause();
+ }
+ });
+ video.play();
+ this.setState({ paused: false });
+ } else {
+ video.pause();
+ this.setState({ paused: true });
+ }
+ });
+ });
+
+ if (currentMedia !== null) {
+ currentMedia.pause();
}
+
+ this.video.play();
+ setCurrentMedia(this.video);
};
toggleFullscreen = () => {
diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js
index cbb4933bc7..f145e1dcaa 100644
--- a/app/javascript/mastodon/reducers/media_attachments.js
+++ b/app/javascript/mastodon/reducers/media_attachments.js
@@ -2,6 +2,13 @@ import { Map as ImmutableMap } from 'immutable';
import { STORE_HYDRATE } from '../actions/store';
+export let currentMedia = null;
+
+export function setCurrentMedia(value) {
+ currentMedia = value;
+}
+
+
const initialState = ImmutableMap({
accept_content_types: [],
});