Skip to content

feat:inline media players for audio/video - #1677

Open
JoshuaVulcan wants to merge 3 commits into
developfrom
ERA-13787
Open

feat:inline media players for audio/video#1677
JoshuaVulcan wants to merge 3 commits into
developfrom
ERA-13787

Conversation

@JoshuaVulcan

Copy link
Copy Markdown
Collaborator

[draft]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds inline playback for audio/video attachments (authenticated blob fetch → object URLs) in both the Report Manager attachment field and the Detail View Activity section, and extends the existing ImageModal to support fullscreen video rendering.

Changes:

  • Introduces fetchFileAsObjectUrlFromUrl() utility to fetch media as a blob and create an object URL.
  • Replaces download-only UX for saved audio/video attachments with inline players (and fullscreen video in Activity section).
  • Adds styling + i18n strings and expands test coverage for the new media behaviors.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/utils/file/index.js Adds fetchFileAsObjectUrlFromUrl() helper for authenticated blob fetching + object URL creation.
src/utils/file/index.test.js Adds unit test coverage for the new object-URL helper.
src/ReportManager/DetailsSection/SchemaForm/formElements/Attachment/index.js Adds play/close UX and inline <audio>/<video> rendering for saved media attachments.
src/ReportManager/DetailsSection/SchemaForm/formElements/Attachment/index.test.js Updates tests to expect play/close behavior and loading spinner for media.
src/ReportManager/DetailsSection/SchemaForm/formElements/Attachment/styles.module.scss Adds layout + player styling and a loading spinner for inline media.
src/DetailViewComponents/ActivitySection/index.js Expands “Expand All” behavior to include audio/video attachments (not just images).
src/DetailViewComponents/ActivitySection/index.test.js Adds test ensuring Expand All expands audio/video attachment collapses.
src/DetailViewComponents/ActivitySection/styles.module.scss Adds media preview sizing styles and a loading spinner animation.
src/DetailViewComponents/ActivitySection/AttachmentListItem/index.js Adds audio/video preview rendering, authenticated media fetch via object URLs, and fullscreen video modal support.
src/DetailViewComponents/ActivitySection/AttachmentListItem/index.test.js Adds extensive tests for audio/video preview, loading state, and fullscreen video modal update flow.
src/ImageModal/index.js Adds mediaType prop and renders <video> when mediaType === 'video'.
src/ImageModal/index.test.js New tests for video rendering, error state, and download behavior.
src/ImageModal/styles.module.scss Extends sizing rules to apply to video as well as img.
public/locales/en-US/reports.json Adds play/close/player ARIA labels for SchemaForm attachments.
public/locales/es/reports.json Same as above (es).
public/locales/fr/reports.json Same as above (fr).
public/locales/ne-NP/reports.json Same as above (ne-NP).
public/locales/pt/reports.json Same as above (pt).
public/locales/sw/reports.json Same as above (sw).
public/locales/en-US/details-view.json Adds audio/video preview alt strings for Activity section attachment previews.
public/locales/es/details-view.json Same as above (es).
public/locales/fr/details-view.json Same as above (fr).
public/locales/ne-NP/details-view.json Same as above (ne-NP).
public/locales/pt/details-view.json Same as above (pt).
public/locales/sw/details-view.json Same as above (sw).

Comment thread src/utils/file/index.test.js
Comment on lines +115 to +121
useEffect(() => {
if (isPlayerOpen && isMedia && attachment.originalUrl && !mediaFetchPromiseRef.current) {
mediaFetchPromiseRef.current = fetchFileAsObjectUrlFromUrl(attachment.originalUrl).then((objectUrl) => {
setMediaObjectUrl(objectUrl);
});
}
}, [attachment.originalUrl, isMedia, isPlayerOpen]);
Comment on lines +123 to +127
useEffect(() => () => {
if (mediaObjectUrl) {
URL.revokeObjectURL(mediaObjectUrl);
}
}, [mediaObjectUrl]);
Comment on lines +56 to +66
const ensureMediaObjectUrl = useCallback(() => {
if (!mediaFetchPromiseRef.current) {
mediaFetchPromiseRef.current = fetchFileAsObjectUrlFromUrl(attachment.url).then((objectUrl) => {
setMediaObjectUrl(objectUrl);

return objectUrl;
});
}

return mediaFetchPromiseRef.current;
}, [attachment.url]);
Comment thread src/ImageModal/index.js Outdated
Comment on lines +77 to +87
{!error && mediaType === 'video' && <video
aria-label={title}
controls
onError={setImageError}
onLoadedData={setImageLoaded}
ref={imageRef}
src={src}
style={{ display: loaded ? 'block' : 'none' }}
/>}

{!error && mediaType === 'image' && <img
Comment thread public/locales/ne-NP/details-view.json Outdated

@luixlive luixlive left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some feedback from me and some from Claude. Candidly, some of these changes feel like a patch over our implementation for images, with hardcoded conditions and complex flows with several useEffects and different fetching and error branches. I feel like it would be cleaner to have specific methods and components per file type so the code is easier to understand, which also makes it less error-prone and easier to test. Of course we can have shared files to avoid repetition.

"collapseOpenButtonLabel": "Collapse",
"collapseOpenButtonTitle": "Expanded",
"imagePreviewAlt": "{{fileName}} preview",
"videoPreviewAlt": "{{fileName}} video preview",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Sort the keys alphabetically so it's easier to search for stuff 👍

});
}

return mediaFetchPromiseRef.current;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this returning the ref? The places from where it is called are not storing it, and the refs are anyway accessible from anywhere. It's a bit misleading.

Comment thread src/ImageModal/index.js
{!loaded && <LoadingOverlay />}
{!loaded && !fetchError && <LoadingOverlay />}

{!showError && mediaType === 'video' && <video

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is called ImageModal. The clean solution here would be to either have a different specific modal for video (if worth it), or renaming this one to a more generic name.


const onShowImageFullScreen = useCallback((event) => {
const mediaFetchPromiseRef = useRef(null);
const pendingModalIdRef = useRef(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Claude:
A single pendingModalIdRef is overwritten on every fullscreen click, so opening the same video's fullscreen modal twice in quick succession leaves the first modal permanently stuck showing the loading spinner.


useEffect(() => () => {
if (mediaObjectUrl) {
URL.revokeObjectURL(mediaObjectUrl);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Claude:
The object URL created for video/audio playback is revoked on component unmount, but a fullscreen ImageModal referencing that same URL lives in global Redux modal state and isn't closed when the detail view unmounts.


$cardExpansionTransitionTime: 300ms;

@keyframes mediaLoadingSpin {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From Claude:
A new hand-rolled CSS spinner (mediaLoadingSpin keyframes + .mediaLoadingSpinner) duplicates the existing LoadingOverlay component that this same PR already uses one file away.

? <span className={styles.error}>{t('playerErrorLabel')}</span>
: mediaObjectUrl
? (attachment.fileType === 'audio'
? <audio

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we have like 3 nested ternary operators, which makes it hard to reason about. We should abstract some of this code into separate components or methods.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants