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: [], });