diff options
author | Sophie Zhang <sophie_zhang@brown.edu> | 2023-07-11 13:30:51 -0400 |
---|---|---|
committer | Sophie Zhang <sophie_zhang@brown.edu> | 2023-07-11 13:30:51 -0400 |
commit | c1df53a7616ccbb9afad2deaf3026e70f3e974b4 (patch) | |
tree | ff3ed78bf5de39a31eeaad73d9a44d373aae0185 /src/client/util/reportManager/ReportManagerComponents.tsx | |
parent | b1f189ffc7dfe558d5895c8f0cb103ab3e5c17d7 (diff) |
starting connecting to new upload endpoint and standardized displaying media
Diffstat (limited to 'src/client/util/reportManager/ReportManagerComponents.tsx')
-rw-r--r-- | src/client/util/reportManager/ReportManagerComponents.tsx | 162 |
1 files changed, 116 insertions, 46 deletions
diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx index 1a4ddb3a3..21c0cb43c 100644 --- a/src/client/util/reportManager/ReportManagerComponents.tsx +++ b/src/client/util/reportManager/ReportManagerComponents.tsx @@ -5,12 +5,16 @@ import ReactMarkdown from 'react-markdown'; import rehypeRaw from 'rehype-raw'; import remarkGfm from 'remark-gfm'; -// Mini components to render issues +/** + * Mini components to render issues. + */ interface IssueCardProps { issue: Issue; onSelect: () => void; } + +// Component for the issue cards list on the left export const IssueCard = ({ issue, onSelect }: IssueCardProps) => { return ( <div className="issue-card" onClick={onSelect}> @@ -33,62 +37,25 @@ interface IssueViewProps { issue: Issue; } +// Detailed issue view that displays on the right export const IssueView = ({ issue }: IssueViewProps) => { const [issueBody, setIssueBody] = React.useState(''); - const isVideoValid = (src: string) => { - const videoElement = document.createElement('video'); - const validPromise: Promise<boolean> = new Promise(resolve => { - videoElement.addEventListener('loadeddata', () => resolve(true)); - videoElement.addEventListener('error', () => resolve(false)); - }); - videoElement.src = src; - return validPromise; - }; - - const getLinkFromTag = async (tag: string) => { - const regex = /src="([^"]+)"/; - let url = ''; - const match = tag.match(regex); - if (match) { - url = match[1]; - } - - if (url.startsWith('https://github.com/brown-dash/Dash-Web/assets')) { - return `\n${url} (Not authorized to display image here)`; - } - return await getTagFromUrl(url); - }; - - const getTagFromUrl = async (url: string) => { - const imgRegex = /https:\/\/browndash\.com\/files[/\\]images/; - const videoRegex = /https:\/\/browndash\.com\/files[/\\]videos/; - const audioRegex = /https:\/\/browndash\.com\/files[/\\]audio/; - - if (imgRegex.test(url) || url.includes('user-images.githubusercontent.com')) { - return `\n${url}\n<img width="100%" alt="Issue asset" src=${url} />\n`; - } else if (videoRegex.test(url)) { - const videoValid = await isVideoValid(url); - if (!videoValid) return `\n${url} (This video could not be loaded)\n`; - return `\n${url}\n<video class="report-default-video" width="100%" controls alt="Issue asset" src=${url} />\n`; - } else if (audioRegex.test(url)) { - return `\n${url}\n<audio src=${url} controls />\n`; - } else { - return url; - } - }; - + // Parses the issue body into a formatted markdown (main functionality is replacing urls with tags) const parseBody = async (body: string) => { const imgTagRegex = /<img\b[^>]*\/?>/; + const videoTagRegex = /<video\b[^>]*\/?>/; + const audioTagRegex = /<audio\b[^>]*\/?>/; + const fileRegex = /https:\/\/browndash\.com\/files/; const parts = body.split('\n'); const modifiedParts = await Promise.all( parts.map(async part => { - if (imgTagRegex.test(part)) { - return `\n${await getLinkFromTag(part)}\n`; + if (imgTagRegex.test(part) || videoTagRegex.test(part) || audioTagRegex.test(part)) { + return `\n${await parseFileTag(part)}\n`; } else if (fileRegex.test(part)) { - const tag = await getTagFromUrl(part); + const tag = await parseDashFiles(part); return tag; } else { return part; @@ -99,6 +66,108 @@ export const IssueView = ({ issue }: IssueViewProps) => { setIssueBody(modifiedParts.join('\n')); }; + // Extracts the src from an image tag and either returns the raw url if not accessible or a new image tag + const parseFileTag = async (tag: string): Promise<string> => { + const regex = /src="([^"]+)"/; + let url = ''; + const match = tag.match(regex); + if (!match) return tag; + url = match[1]; + if (!url) return tag; + + const mimeType = url.split('.').pop(); + if (!mimeType) return tag; + + switch (mimeType) { + // image + case '.jpg': + case '.png': + case '.jpeg': + case '.gif': + return await getDisplayedFile(url, 'image'); + // video + case '.mp4': + case '.mpeg': + case '.webm': + case '.mov': + return await getDisplayedFile(url, 'video'); + //audio + case '.mp3': + case '.wav': + case '.ogg': + return await getDisplayedFile(url, 'audio'); + } + return tag; + }; + + // Returns the corresponding HTML tag for a src url + const parseDashFiles = async (url: string) => { + const dashImgRegex = /https:\/\/browndash\.com\/files[/\\]images/; + const dashVideoRegex = /https:\/\/browndash\.com\/files[/\\]videos/; + const dashAudioRegex = /https:\/\/browndash\.com\/files[/\\]audio/; + + if (dashImgRegex.test(url)) { + return await getDisplayedFile(url, 'image'); + } else if (dashVideoRegex.test(url)) { + return await getDisplayedFile(url, 'video'); + } else if (dashAudioRegex.test(url)) { + return await getDisplayedFile(url, 'audio'); + } else { + return url; + } + }; + + const getDisplayedFile = async (url: string, fileType: 'image' | 'video' | 'audio'): Promise<string> => { + switch (fileType) { + case 'image': + const imgValid = await isImgValid(url); + if (!imgValid) return `\n${url} (This image could not be loaded)\n`; + return `\n${url}\n<img width="100%" alt="Issue asset" src=${url} />\n`; + case 'video': + const videoValid = await isVideoValid(url); + if (!videoValid) return `\n${url} (This video could not be loaded)\n`; + return `\n${url}\n<video class="report-default-video" width="100%" controls alt="Issue asset" src=${url} />\n`; + case 'audio': + const audioValid = await isAudioValid(url); + if (!audioValid) return `\n${url} (This audio could not be loaded)\n`; + return `\n${url}\n<audio src=${url} controls />\n`; + } + }; + + // Loads an image and returns a promise that resolves as whether the image is valid or not + const isImgValid = (src: string) => { + const imgElement = document.createElement('video'); + const validPromise: Promise<boolean> = new Promise(resolve => { + imgElement.addEventListener('load', () => resolve(true)); + imgElement.addEventListener('error', () => resolve(false)); + }); + imgElement.src = src; + return validPromise; + }; + + // Loads a video and returns a promise that resolves as whether the video is valid or not + const isVideoValid = (src: string) => { + const videoElement = document.createElement('video'); + const validPromise: Promise<boolean> = new Promise(resolve => { + videoElement.addEventListener('loadeddata', () => resolve(true)); + videoElement.addEventListener('error', () => resolve(false)); + }); + videoElement.src = src; + return validPromise; + }; + + // Loads audio and returns a promise that resolves as whether the audio is valid or not + const isAudioValid = (src: string) => { + const audioElement = document.createElement('audio'); + const validPromise: Promise<boolean> = new Promise(resolve => { + audioElement.addEventListener('loadeddata', () => resolve(true)); + audioElement.addEventListener('error', () => resolve(false)); + }); + audioElement.src = src; + return validPromise; + }; + + // Called on mount to parse the body React.useEffect(() => { parseBody((issue.body as string) ?? ''); }, [issue]); @@ -141,6 +210,7 @@ interface TagProps { onClick?: () => void; } +// Small tag for labels of the issue export const Tag = ({ text, color, backgroundColor, fontSize, border, borderColor, onClick }: TagProps) => { return ( <div |