Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/app/hooks/usePolling/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useCallback, useEffect, useRef, useState } from 'react';

/**
* This interval determines how often the polling function will be triggered. In milliseconds.
*/
const POLLING_INTERVAL = 15000;

/**
* @type {typeof import('./types.d.ts').usePolling}
*/
export default (hasPolling: boolean) => {
const [forceUpdate, setForceUpdate] = useState(false);
const [pageHasEnded, setPageHasEnded] = useState(false);

Check failure on line 13 in src/app/hooks/usePolling/index.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

'setPageHasEnded' is declared but its value is never read.

Check failure on line 13 in src/app/hooks/usePolling/index.ts

View workflow job for this annotation

GitHub Actions / cypress-run (22.x)

'setPageHasEnded' is declared but its value is never read.

Check failure on line 13 in src/app/hooks/usePolling/index.ts

View workflow job for this annotation

GitHub Actions / cypress-run (22.x)

'setPageHasEnded' is declared but its value is never read.

Check failure on line 13 in src/app/hooks/usePolling/index.ts

View workflow job for this annotation

GitHub Actions / cypress-run (22.x)

'setPageHasEnded' is declared but its value is never read.
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);

const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
}, []);

const startPolling = useCallback(() => {
if (pollTimerRef.current) {
return;
}

pollTimerRef.current = setInterval(() => {
setForceUpdate(true);
}, POLLING_INTERVAL);
}, []);

// const stopPollingIfPageEnded = useCallback(
// (endTimeDate: string | Date) => {
// if (endTimeDate && !pageHasEnded) {
// if (new Date() > new Date(endTimeDate)) {
// setPageHasEnded(true);
// stopPolling();
// }
// }
// },
// [pageHasEnded, stopPolling],
// );

// const stopPollingIfFeatureToggleOff = useCallback(
// (pollingEnabled: boolean) => {
// if (pollingEnabled === false && !pageHasEnded) {
// setPageHasEnded(true);
// stopPolling();
// }
// },
// [pageHasEnded, stopPolling],
// );

const updateFinished = useCallback(() => {
setForceUpdate(false);
}, []);

useEffect(() => {
if (hasPolling && !pageHasEnded) {
startPolling();
}

return () => {
stopPolling();
};
}, [hasPolling, pageHasEnded, startPolling, stopPolling]);

return {
forceUpdate,
updateFinished,
stopPolling,
// stopPollingIfPageEnded,
// stopPollingIfFeatureToggleOff,
pageHasEnded,
};
};
46 changes: 46 additions & 0 deletions src/app/hooks/usePolling/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* The return object from the `usePolling` hook.
*/
type PollingResult = {
/*
* Indicates if an update should happen.
*/
forceUpdate: boolean;

/**
* Indicate that an update has happened.
*/
updateFinished: () => void;

/**
* A function to stop polling.
*/
stopPolling: () => void;

/**
* Function to stop polling if the given date time has passed.
*
* @param endTimeDate - Set a date time in microseconds, parsable date time string or `Date` instance. If not set, polling will not end.
*/
stopPollingIfPageEnded: (endTimeDate?: number | string | Date) => void;

/**
* Function to stop polling if the feature flag was set to `false`.
*
* @param pollingEnabled - Set to `false` in order to stop polling.
*/
stopPollingIfFeatureToggleOff: (pollingEnabled?: boolean) => void;

/**
* Indicates if polling for this page has ended.
*/
pageHasEnded: boolean;
};

/**
* A custom React hook called `usePolling` designed to handle polling functionality, where a component can periodically request updates or refresh data from a server.
*
* @param hasPolling - To determine whether polling should be enabled or not.
* @returns A polling result object.
*/
declare const usePolling: (hasPolling: boolean) => PollingResult;
31 changes: 31 additions & 0 deletions src/app/hooks/usePollingFake/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback, useEffect, useState } from 'react';

const FAKE_POLLING_INTERVAL = 5000;

let updateCount = 0;

const usePollingFake = () => {
const [forceUpdate, setForceUpdate] = useState(false);

const updateFinished = useCallback(() => {
setForceUpdate(false);
}, []);

useEffect(() => {
const timerId = setInterval(() => {
console.log('Timer started');
setForceUpdate(true);
updateCount += 1;
console.log(`Timer triggered: ${updateCount} time(s)`);
}, FAKE_POLLING_INTERVAL);

return () => clearTimeout(timerId);
}, []);

return {
forceUpdate,
updateFinished,
};
};

export default usePollingFake;
46 changes: 46 additions & 0 deletions src/app/hooks/usePollingFake/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* The return object from the `usePolling` hook.
*/
type PollingResult = {
/*
* Indicates if an update should happen.
*/
forceUpdate: boolean;

/**
* Indicate that an update has happened.
*/
updateFinished: () => void;

/**
* A function to stop polling.
*/
stopPolling: () => void;

/**
* Function to stop polling if the given date time has passed.
*
* @param endTimeDate - Set a date time in microseconds, parsable date time string or `Date` instance. If not set, polling will not end.
*/
stopPollingIfPageEnded: (endTimeDate?: number | string | Date) => void;

/**
* Function to stop polling if the feature flag was set to `false`.
*
* @param pollingEnabled - Set to `false` in order to stop polling.
*/
stopPollingIfFeatureToggleOff: (pollingEnabled?: boolean) => void;

/**
* Indicates if polling for this page has ended.
*/
pageHasEnded: boolean;
};

/**
* A custom React hook called `usePolling` designed to handle polling functionality, where a component can periodically request updates or refresh data from a server.
*
* @param hasPolling - To determine whether polling should be enabled or not.
* @returns A polling result object.
*/
declare const usePolling: (hasPolling: boolean) => PollingResult;
2 changes: 2 additions & 0 deletions src/app/models/types/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface Translations {
shareButtonText: string;
postDateTimeFormat?: string;
postDateFormat?: string;
refreshButtonText?: string;
visuallyHiddenButtonText?: string;
};
downloads?: {
instructions?: string;
Expand Down
118 changes: 118 additions & 0 deletions ws-nextjs-app/pages/[service]/live/[id]/LatestPostButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { RefObject } from 'react';
import { use, useCallback, useEffect, useState, useRef } from 'react';
import { ServiceContext } from '#app/contexts/ServiceContext';
import VisuallyHiddenText from '#app/components/VisuallyHiddenText';
import styles from './styles';

const RefreshSvg = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M31.1 2.5H1v2.8h30.1zM14.3 13.3 31 29.5v-6.7L16 8.1.9 22.8v6.7l16.8-16.2z" />
</svg>
);

interface LatestPostButtonProps {
streamRef: RefObject<HTMLDivElement>;
isFirstPostVisible: boolean;
hasPendingUpdate: boolean;
}

const LatestPostButton = ({
streamRef,
isFirstPostVisible,
hasPendingUpdate,
}: LatestPostButtonProps) => {
const [leftPosition, setLeftPosition] = useState('50%');
const buttonRef = useRef<HTMLButtonElement>(null);
const resizeTimeoutRef = useRef<NodeJS.Timeout>(null);

const {
translations: {
liveExperiencePage: {
refreshButtonText = 'Latest Post',
visuallyHiddenButtonText = 'New post available',
},
},
} = use(ServiceContext);

const updatePosition = useCallback(() => {
if (!streamRef?.current) {
setLeftPosition('50%');
return;
}

const streamContainerWidth = streamRef.current.clientWidth;
const streamContainerLeftPosition =
streamRef.current.getBoundingClientRect().left;

if (streamContainerWidth !== 0) {
setLeftPosition(
`${streamContainerLeftPosition + streamContainerWidth / 2}px`,
);
} else {
setLeftPosition('50%');
}
}, [streamRef]);

useEffect(() => {
updatePosition();

const handleResize = () => {
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = setTimeout(updatePosition, 100);
};

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, [updatePosition]);

const handleClick = async () => {
const hasReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;

const streamContainer = document.getElementById('stream-container');
if (streamContainer) {
streamContainer.scrollIntoView({
behavior: hasReducedMotion ? 'auto' : 'smooth',
});
}
};

const showButton = !isFirstPostVisible && hasPendingUpdate;

return (
<>
<VisuallyHiddenText aria-live="polite">
{showButton && <span>{visuallyHiddenButtonText}</span>}
</VisuallyHiddenText>
<button
ref={buttonRef}
type="button"
onClick={handleClick}
css={styles.button}
tabIndex={showButton ? 0 : -1}
aria-live="polite"
aria-atomic="true"
style={{
left: `${leftPosition}`,
display: showButton ? 'inline-flex' : 'none',
transform:
leftPosition === '50%' ? 'translateX(-50%)' : 'translateX(-50%)',
}}
>
<RefreshSvg />
<span>{refreshButtonText}</span>
</button>
</>
);
};

export default LatestPostButton;
39 changes: 39 additions & 0 deletions ws-nextjs-app/pages/[service]/live/[id]/LatestPostButton/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Theme, css } from '@emotion/react';
// eslint-disable-next-line import/no-relative-packages
import pixelsToRem from '../../../../../../src/app/utilities/pixelsToRem';

const styles = {
button: ({ palette, fontSizes, fontVariants, spacings, mq }: Theme) =>
css({
position: 'fixed',
display: 'inline-flex',
alignItems: 'center',
top: `${spacings.TRIPLE}rem`,
color: palette.WHITE,
...fontSizes.pica,
...fontVariants.sansBold,
padding: `${pixelsToRem(12)}rem ${pixelsToRem(20)}rem`,
borderRadius: '500px',
border: 'none',
backgroundColor: palette.BRAND_BACKGROUND,
cursor: 'pointer',
zIndex: 9999,
'&:hover, &:focus': {
color: palette.WHITE,
textDecoration: 'underline',
textUnderlineOffset: `${pixelsToRem(4)}rem`,
},
svg: {
width: `${spacings.DOUBLE}rem`,
height: `${spacings.DOUBLE}rem`,
marginInlineEnd: `${spacings.FULL}rem`,
path: {
fill: palette.WHITE,
[mq.FORCED_COLOURS]: {
fill: 'canvasText',
},
},
},
}),
};
export default styles;
Loading
Loading