From 3265a211f987d773a577c97b7da9456c57547fc0 Mon Sep 17 00:00:00 2001 From: vanessa <32312712+vlbee@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:20:38 +0000 Subject: [PATCH 1/6] remove ab testing from ListenToArticle --- .../components/ListenToArticle.importable.tsx | 43 +++++-------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/dotcom-rendering/src/components/ListenToArticle.importable.tsx b/dotcom-rendering/src/components/ListenToArticle.importable.tsx index 803f573314c..af6474f06b2 100644 --- a/dotcom-rendering/src/components/ListenToArticle.importable.tsx +++ b/dotcom-rendering/src/components/ListenToArticle.importable.tsx @@ -1,9 +1,6 @@ import { log } from '@guardian/libs'; import { useEffect, useState } from 'react'; -import { - getListenToArticleClient, - getNativeABTestingClient, -} from '../lib/bridgetApi'; +import { getListenToArticleClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { ListenToArticleButton } from './ListenToArticleButton'; @@ -37,39 +34,19 @@ export const ListenToArticle = ({ articleId }: Props) => { useEffect(() => { if (isBridgetCompatible) { Promise.all([ - // AB TESTING native - getNativeABTestingClient().getParticipations(), getListenToArticleClient().isAvailable(articleId), getListenToArticleClient().isPlaying(articleId), getListenToArticleClient().getAudioDurationSeconds(articleId), ]) - .then( - ([ - abParticipations, - isAvailable, - isPlaying, - durationSeconds, - ]) => { - // AB TESTING native start - const variant = abParticipations.get( - 'l2a_article_button_test', - ); - if (variant === 'no-button') { - setShowButton(false); - } else if (variant === 'with-duration') { - setAudioDurationSeconds( - typeof durationSeconds === 'number' && - durationSeconds > 0 - ? durationSeconds - : undefined, - ); - setShowButton(isAvailable && !isPlaying); - } else if (variant === 'without-duration') { - setShowButton(isAvailable && !isPlaying); - } - // AB TESTING native ends - }, - ) + .then(([isAvailable, isPlaying, durationSeconds]) => { + setAudioDurationSeconds( + typeof durationSeconds === 'number' && + durationSeconds > 0 + ? durationSeconds + : undefined, + ); + setShowButton(isAvailable && !isPlaying); + }) .catch((error) => { console.error( 'Error fetching article audio status: ', From 5d2b975fedd49ded9d44cf5f08b941e554152ffa Mon Sep 17 00:00:00 2001 From: vanessa <32312712+vlbee@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:21:24 +0000 Subject: [PATCH 2/6] Add waveform container to ListenToAudio button --- .../components/ListenToArticle.importable.tsx | 4 +- .../ListenToArticleButton.stories.tsx | 2 +- .../src/components/ListenToArticleButton.tsx | 83 +++++++++++++------ 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/dotcom-rendering/src/components/ListenToArticle.importable.tsx b/dotcom-rendering/src/components/ListenToArticle.importable.tsx index af6474f06b2..80fd03cc973 100644 --- a/dotcom-rendering/src/components/ListenToArticle.importable.tsx +++ b/dotcom-rendering/src/components/ListenToArticle.importable.tsx @@ -25,10 +25,10 @@ export const formatAudioDuration = ( }; export const ListenToArticle = ({ articleId }: Props) => { - const [showButton, setShowButton] = useState(false); + const [showButton, setShowButton] = useState(true); const [audioDurationSeconds, setAudioDurationSeconds] = useState< number | undefined - >(undefined); + >(314); const isBridgetCompatible = useIsBridgetCompatible('8.7.0'); useEffect(() => { diff --git a/dotcom-rendering/src/components/ListenToArticleButton.stories.tsx b/dotcom-rendering/src/components/ListenToArticleButton.stories.tsx index 683c2930c20..7627da6c6e5 100644 --- a/dotcom-rendering/src/components/ListenToArticleButton.stories.tsx +++ b/dotcom-rendering/src/components/ListenToArticleButton.stories.tsx @@ -13,7 +13,7 @@ type Story = StoryObj; export const ListenToArticleWithDurationButton = { args: { onClickHandler: () => undefined, - audioDuration: '3:02', + audioDuration: '5:14', }, } satisfies Story; diff --git a/dotcom-rendering/src/components/ListenToArticleButton.tsx b/dotcom-rendering/src/components/ListenToArticleButton.tsx index 3c24d4f6d02..820ba9e68b9 100644 --- a/dotcom-rendering/src/components/ListenToArticleButton.tsx +++ b/dotcom-rendering/src/components/ListenToArticleButton.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/react'; -import { height, space } from '@guardian/source/foundations'; +import { height, neutral, space } from '@guardian/source/foundations'; +import type { ThemeIcon } from '@guardian/source/react-components'; import { Button, SvgMediaControlsPlay, @@ -18,12 +19,12 @@ const buttonCss = (audioDuration: string | undefined) => css` } margin-bottom: ${space[4]}px; margin-left: ${space[2]}px; - padding-left: ${space[2]}px; + padding-left: ${space[3]}px; padding-right: ${audioDuration === undefined ? space[4] : space[3]}px; padding-bottom: 0px; font-size: 15px; - height: ${height.ctaSmall}px; - min-height: ${height.ctaSmall}px; + height: ${height.ctaXsmall}px; + min-height: ${height.ctaXsmall}px; .src-button-space { width: 0px; @@ -36,15 +37,46 @@ const dividerCss = css` opacity: 0.5; border-left: 1px solid ${palette('--follow-icon-background')}; margin-left: ${space[2]}px; - margin-right: ${space[2]}px; `; -const durationCss = css` - font-weight: 300; +const themeIcon: ThemeIcon = { + fill: palette('--follow-icon-background'), +}; + +const waveFormContainerCss = css` + height: ${space[12]}px; + border-top: 1px solid ${neutral[86]}; `; -const baselineCss = css` - padding-bottom: 2px; +const generateWaveformGradients = (barCount: number): string => { + const barWidth = 2; + const spacing = 1; + const gradients: string[] = []; + let lastBarHeight = Math.floor(Math.random() * 60) + 25; // Initial random height + + for (let i = 0; i < barCount; i++) { + const variation = lastBarHeight * 0.5; // 50% of last bar height + const minHeight = Math.max(50, lastBarHeight - variation); + const maxHeight = Math.min(70, lastBarHeight + variation); + const barHeight = + Math.floor(Math.random() * (maxHeight - minHeight + 1)) + minHeight; + lastBarHeight = barHeight; + const position = i * (barWidth + spacing); + gradients.push( + `linear-gradient(to top, ${neutral[86]} 0 ${barHeight}%, transparent ${barHeight}%) ${position}px 100% / ${barWidth}px 100%`, + ); + } + + return gradients.join(',\n\t\t'); +}; + +const waveFormCss = css` + background: ${generateWaveformGradients(250)}; + background-repeat: no-repeat; + height: inherit; + display: block; + width: 100%; + padding-top: ${space[2]}px; `; type ButtonProps = { @@ -55,19 +87,22 @@ export const ListenToArticleButton = ({ onClickHandler, audioDuration, }: ButtonProps) => ( - +
+
+ +
+
); From 9650f167fc95d2b222dd1d2522b1b8636583b015 Mon Sep 17 00:00:00 2001 From: vanessa <32312712+vlbee@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:51:32 +0000 Subject: [PATCH 3/6] fix soundwave --- dotcom-rendering/src/components/ListenToArticleButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/components/ListenToArticleButton.tsx b/dotcom-rendering/src/components/ListenToArticleButton.tsx index 820ba9e68b9..014bc95eabc 100644 --- a/dotcom-rendering/src/components/ListenToArticleButton.tsx +++ b/dotcom-rendering/src/components/ListenToArticleButton.tsx @@ -55,7 +55,7 @@ const generateWaveformGradients = (barCount: number): string => { let lastBarHeight = Math.floor(Math.random() * 60) + 25; // Initial random height for (let i = 0; i < barCount; i++) { - const variation = lastBarHeight * 0.5; // 50% of last bar height + const variation = lastBarHeight * 0.5; // keep within 50% of last bar height const minHeight = Math.max(50, lastBarHeight - variation); const maxHeight = Math.min(70, lastBarHeight + variation); const barHeight = @@ -63,7 +63,7 @@ const generateWaveformGradients = (barCount: number): string => { lastBarHeight = barHeight; const position = i * (barWidth + spacing); gradients.push( - `linear-gradient(to top, ${neutral[86]} 0 ${barHeight}%, transparent ${barHeight}%) ${position}px 100% / ${barWidth}px 100%`, + `linear-gradient(to top, ${neutral[86]} 0 ${barHeight}%, transparent ${barHeight}%) ${position}px 50% / ${barWidth}px 100%`, ); } From e485f690e5957b2a3d39f68e2520d0a8b9b9a9bd Mon Sep 17 00:00:00 2001 From: vanessa <32312712+vlbee@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:00:59 +0000 Subject: [PATCH 4/6] Use podcast wave component --- .../src/components/ListenToArticleButton.tsx | 71 ++++++++++--------- dotcom-rendering/src/components/WaveForm.tsx | 10 +-- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/dotcom-rendering/src/components/ListenToArticleButton.tsx b/dotcom-rendering/src/components/ListenToArticleButton.tsx index 014bc95eabc..5421f534ca1 100644 --- a/dotcom-rendering/src/components/ListenToArticleButton.tsx +++ b/dotcom-rendering/src/components/ListenToArticleButton.tsx @@ -6,6 +6,8 @@ import { SvgMediaControlsPlay, } from '@guardian/source/react-components'; import { palette } from '../palette'; +import type { WaveFormProps, WaveFormTheme } from './WaveForm'; +import { WaveForm } from './WaveForm'; const buttonCss = (audioDuration: string | undefined) => css` display: flex; @@ -46,49 +48,52 @@ const themeIcon: ThemeIcon = { const waveFormContainerCss = css` height: ${space[12]}px; border-top: 1px solid ${neutral[86]}; -`; - -const generateWaveformGradients = (barCount: number): string => { - const barWidth = 2; - const spacing = 1; - const gradients: string[] = []; - let lastBarHeight = Math.floor(Math.random() * 60) + 25; // Initial random height + position: relative; + padding-top: ${space[2]}px; + overflow: hidden; - for (let i = 0; i < barCount; i++) { - const variation = lastBarHeight * 0.5; // keep within 50% of last bar height - const minHeight = Math.max(50, lastBarHeight - variation); - const maxHeight = Math.min(70, lastBarHeight + variation); - const barHeight = - Math.floor(Math.random() * (maxHeight - minHeight + 1)) + minHeight; - lastBarHeight = barHeight; - const position = i * (barWidth + spacing); - gradients.push( - `linear-gradient(to top, ${neutral[86]} 0 ${barHeight}%, transparent ${barHeight}%) ${position}px 50% / ${barWidth}px 100%`, - ); + > svg { + position: absolute; + top: ${space[2]}px; + left: 0; + width: 746px; /* Fixed width - adjust as needed */ + height: 100%; + z-index: 0; } - return gradients.join(',\n\t\t'); -}; - -const waveFormCss = css` - background: ${generateWaveformGradients(250)}; - background-repeat: no-repeat; - height: inherit; - display: block; - width: 100%; - padding-top: ${space[2]}px; + > button { + position: relative; + z-index: 1; + } `; type ButtonProps = { onClickHandler: () => void; audioDuration?: string; }; + +const waveTheme: WaveFormTheme = { + progress: neutral[86], + buffer: neutral[86], + wave: neutral[86], +}; + +const waveFormProps: WaveFormProps = { + seed: 'listen to this article', + height: space[12] - 8, + bars: 250, + theme: waveTheme, + gap: 1, + barWidth: 2, +}; + export const ListenToArticleButton = ({ onClickHandler, audioDuration, -}: ButtonProps) => ( -
-
+}: ButtonProps) => { + return ( +
+
-
-); + ); +}; diff --git a/dotcom-rendering/src/components/WaveForm.tsx b/dotcom-rendering/src/components/WaveForm.tsx index 02ebd3ae959..4981e6f3ab5 100644 --- a/dotcom-rendering/src/components/WaveForm.tsx +++ b/dotcom-rendering/src/components/WaveForm.tsx @@ -68,19 +68,19 @@ function generateWaveform(seed: string, bars: number, height: number) { return compress(peaks, height * minimumBarHeight); } -type Theme = { +export type WaveFormTheme = { progress?: string; buffer?: string; wave?: string; }; -const defaultTheme: Theme = { +const defaultTheme: WaveFormTheme = { progress: 'green', buffer: 'orange', wave: 'grey', }; -type Props = { +export type WaveFormProps = { /** * The same seed will generate the same waveform. For example, passing the url * as the seed will ensure the waveform is the same for the same audio file. @@ -90,7 +90,7 @@ type Props = { bars: number; progress?: number; buffer?: number; - theme?: Theme; + theme?: WaveFormTheme; gap?: number; barWidth?: number; } & React.SVGProps; @@ -105,7 +105,7 @@ export const WaveForm = ({ gap = 1, barWidth = 4, ...props -}: Props) => { +}: WaveFormProps) => { // memoise the waveform data so they aren't recalculated on every render const barHeights = useMemo( () => generateWaveform(seed, bars, height), From 0e385eb9264f382853280a341f9f39be56160dc4 Mon Sep 17 00:00:00 2001 From: vanessa <32312712+vlbee@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:14:01 +0000 Subject: [PATCH 5/6] make seed unique --- .../src/components/ListenToArticle.importable.tsx | 1 + .../src/components/ListenToArticleButton.tsx | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/dotcom-rendering/src/components/ListenToArticle.importable.tsx b/dotcom-rendering/src/components/ListenToArticle.importable.tsx index e32add1f161..1dd7fb58da1 100644 --- a/dotcom-rendering/src/components/ListenToArticle.importable.tsx +++ b/dotcom-rendering/src/components/ListenToArticle.importable.tsx @@ -84,6 +84,7 @@ export const ListenToArticle = ({ articleId }: Props) => { showButton && ( void; audioDuration?: string; + waveFormSeed?: string; }; const waveTheme: WaveFormTheme = { @@ -78,22 +79,25 @@ const waveTheme: WaveFormTheme = { wave: neutral[86], }; -const waveFormProps: WaveFormProps = { - seed: 'listen to this article', +const waveFormProps = ( + seed: string = 'listen to this article', +): WaveFormProps => ({ + seed, height: space[12] - 8, bars: 250, theme: waveTheme, gap: 1, barWidth: 2, -}; +}); export const ListenToArticleButton = ({ onClickHandler, audioDuration, + waveFormSeed, }: ButtonProps) => { return (
- +