diff --git a/app/soapbox/__fixtures__/intlMessages.json b/app/soapbox/__fixtures__/intlMessages.json index 54c919e6a..eb2a35806 100644 --- a/app/soapbox/__fixtures__/intlMessages.json +++ b/app/soapbox/__fixtures__/intlMessages.json @@ -319,6 +319,7 @@ "poll_button.add_poll": "Add a poll", "poll_button.remove_poll": "Remove poll", "preferences.fields.auto_play_gif_label": "Auto-play animated GIFs", + "preferences.fields.auto_play_video_label": "Auto-play videos", "preferences.fields.boost_modal_label": "Show confirmation dialog before reposting", "preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post", "preferences.fields.demetricator_label": "Use Demetricator", diff --git a/app/soapbox/components/status-media.tsx b/app/soapbox/components/status-media.tsx index 4445e67b5..b159d4fe5 100644 --- a/app/soapbox/components/status-media.tsx +++ b/app/soapbox/components/status-media.tsx @@ -6,7 +6,8 @@ import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder import Card from 'soapbox/features/status/components/card'; import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery, Video, Audio } from 'soapbox/features/ui/util/async-components'; -import { useAppDispatch } from 'soapbox/hooks'; +import { useAppDispatch, useSettings } from 'soapbox/hooks'; +import { addAutoPlay } from 'soapbox/utils/media'; import type { List as ImmutableList } from 'immutable'; import type { Status, Attachment } from 'soapbox/types/entities'; @@ -33,6 +34,9 @@ const StatusMedia: React.FC = ({ onToggleVisibility = () => { }, }) => { const dispatch = useAppDispatch(); + const settings = useSettings(); + const shouldAutoPlayVideo = settings.get('autoPlayVideo'); + const [mediaWrapperWidth, setMediaWrapperWidth] = useState(undefined); const size = status.media_attachments.size; @@ -93,7 +97,9 @@ const StatusMedia: React.FC = ({ ref={setRef} className='status-card__image status-card-video' style={height ? { height } : undefined} - dangerouslySetInnerHTML={{ __html: status.card.html }} + dangerouslySetInnerHTML={{ + __html: shouldAutoPlayVideo ? addAutoPlay(status.card.html) : status.card.html, + }} /> ); diff --git a/app/soapbox/features/preferences/index.tsx b/app/soapbox/features/preferences/index.tsx index 3adb90861..98221a8fd 100644 --- a/app/soapbox/features/preferences/index.tsx +++ b/app/soapbox/features/preferences/index.tsx @@ -193,6 +193,10 @@ const Preferences = () => { + }> + + + {features.spoilers && }> } diff --git a/app/soapbox/features/status/components/card.tsx b/app/soapbox/features/status/components/card.tsx index 3d83020fe..18dd427a5 100644 --- a/app/soapbox/features/status/components/card.tsx +++ b/app/soapbox/features/status/components/card.tsx @@ -5,7 +5,9 @@ import React, { useState, useEffect } from 'react'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; import { HStack, Stack, Text } from 'soapbox/components/ui'; +import { useSettings } from 'soapbox/hooks'; import { normalizeAttachment } from 'soapbox/normalizers'; +import { addAutoPlay } from 'soapbox/utils/media'; import type { Card as CardEntity, Attachment } from 'soapbox/types/entities'; @@ -19,30 +21,6 @@ const trim = (text: string, len: number): string => { return text.substring(0, cut) + (text.length > len ? '…' : ''); }; -const domParser = new DOMParser(); - -const addAutoPlay = (html: string): string => { - const document = domParser.parseFromString(html, 'text/html').documentElement; - const iframe = document.querySelector('iframe'); - - if (iframe) { - if (iframe.src.includes('?')) { - iframe.src += '&'; - } else { - iframe.src += '?'; - } - - iframe.src += 'autoplay=1&auto_play=1'; - iframe.allow = 'autoplay'; - - // DOM parser creates html/body elements around original HTML fragment, - // so we need to get innerHTML out of the body and not the entire document - return (document.querySelector('body') as HTMLBodyElement).innerHTML; - } - - return html; -}; - interface ICard { card: CardEntity, maxTitle?: number, @@ -64,6 +42,9 @@ const Card: React.FC = ({ onOpenMedia, horizontal, }): JSX.Element => { + const settings = useSettings(); + const shouldAutoPlayVideo = settings.get('autoPlayVideo'); + const [width, setWidth] = useState(defaultWidth); const [embedded, setEmbedded] = useState(false); @@ -111,7 +92,7 @@ const Card: React.FC = ({ }; const renderVideo = () => { - const content = { __html: addAutoPlay(card.html) }; + const content = { __html: shouldAutoPlayVideo ? addAutoPlay(card.html) : card.html }; const ratio = getRatio(card); const height = width / ratio; diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index f2eaf32c8..b7c280137 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -790,6 +790,7 @@ "poll_button.remove_poll": "Remove poll", "pre_header.close": "Close", "preferences.fields.auto_play_gif_label": "Auto-play animated GIFs", + "preferences.fields.auto_play_video_label": "Auto-play videos", "preferences.fields.autoload_more_label": "Automatically load more items when scrolled to the bottom of the page", "preferences.fields.autoload_timelines_label": "Automatically load new posts when scrolled to the top of the page", "preferences.fields.boost_modal_label": "Show confirmation dialog before reposting", diff --git a/app/soapbox/utils/__tests__/media.test.ts b/app/soapbox/utils/__tests__/media.test.ts new file mode 100644 index 000000000..56a02aebc --- /dev/null +++ b/app/soapbox/utils/__tests__/media.test.ts @@ -0,0 +1,31 @@ +import { addAutoPlay } from '../media'; + +describe('addAutoPlay()', () => { + describe('when the provider is Rumble', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + + describe('when the iframe src already has params', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + }); + }); + + describe('when the provider is not Rumble', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + + describe('when the iframe src already has params', () => { + it('adds the correct query parameters to the src', () => { + const html = ''; + expect(addAutoPlay(html)).toEqual(''); + }); + }); + }); +}); diff --git a/app/soapbox/utils/media.ts b/app/soapbox/utils/media.ts index 7947ef91f..ff0bf502e 100644 --- a/app/soapbox/utils/media.ts +++ b/app/soapbox/utils/media.ts @@ -51,4 +51,37 @@ const getVideoDuration = (file: File): Promise => { return promise; }; -export { getVideoDuration, formatBytes, truncateFilename }; +const domParser = new DOMParser(); + +enum VideoProviders { + RUMBLE = 'rumble.com' +} + +const addAutoPlay = (html: string): string => { + const document = domParser.parseFromString(html, 'text/html').documentElement; + const iframe = document.querySelector('iframe'); + + if (iframe) { + const url = new URL(iframe.src); + const provider = new URL(iframe.src).host; + + if (provider === VideoProviders.RUMBLE) { + url.searchParams.append('pub', '7a20'); + url.searchParams.append('autoplay', '2'); + } else { + url.searchParams.append('autoplay', '1'); + url.searchParams.append('auto_play', '1'); + iframe.allow = 'autoplay'; + } + + iframe.src = url.toString(); + + // DOM parser creates html/body elements around original HTML fragment, + // so we need to get innerHTML out of the body and not the entire document + return (document.querySelector('body') as HTMLBodyElement).innerHTML; + } + + return html; +}; + +export { getVideoDuration, formatBytes, truncateFilename, addAutoPlay };