sanitizing message for clickable links

This commit is contained in:
Roland Osborne 2024-12-19 17:15:16 -08:00
parent 383961d467
commit 6e0ddc1238
9 changed files with 55 additions and 12 deletions

View File

@ -24,6 +24,7 @@
"@vitejs/plugin-react": "4.3.1",
"crypto-js": "^4.2.0",
"databag-client-sdk": "^0.0.20",
"dompurify": "^3.2.3",
"jest": "29.1.1",
"jsencrypt": "^3.3.2",
"react": "18.3.1",

View File

@ -14,8 +14,7 @@
padding-left: 2px;
padding-right: 2px;
position: absolute;
font-size: 8px;
bottom: 0;
font-size: 12px;
}
.close {

View File

@ -1,5 +1,5 @@
import React from 'react';
import { ActionIcon, Image, Textarea } from '@mantine/core'
import React, { useEffect } from 'react';
import { ActionIcon, Image, Text } from '@mantine/core'
import { useAudioFile } from './useAudioFile.hook';
import classes from './AudioFile.module.css'
import audio from '../../images/audio.png'
@ -16,7 +16,7 @@ export function AudioFile({ source, updateLabel, disabled, remove }: {source: Fi
return (
<div className={classes.asset}>
<Image radius="sm" className={classes.thumb} src={audio} />
<Textarea className={classes.label} size="xs" value={state.label} onChange={(event) => setLabel(event.currentTarget.value)} disabled={disabled} />
<Text className={classes.label}>{ state.label }</Text>
<ActionIcon className={classes.close} variant="subtle" disabled={disabled} onClick={remove}>
<IconX />
</ActionIcon>

View File

@ -13,7 +13,7 @@ export function BinaryFile({ source, disabled, remove }: {source: File, disabled
<Image radius="sm" className={classes.thumb} src={binary} />
<Text className={classes.name}>{ state.name }</Text>
<Text className={classes.extension}>{ state.extension }</Text>
{ !state.disabled && (
{ !disabled && (
<ActionIcon className={classes.close} variant="subtle" onClick={remove}>
<IconX />
</ActionIcon>

View File

@ -1,4 +1,4 @@
import { useRef, useState, useCallback } from 'react';
import { useRef, useEffect, useState, useCallback } from 'react';
import { avatar } from '../constants/Icons'
import { Topic, Card, Profile } from 'databag-client-sdk';
import classes from './Message.module.css'
@ -11,6 +11,7 @@ import type { MediaAsset } from '../conversation/Conversation';
import { useMessage } from './useMessage.hook';
import { IconForbid, IconTrash, IconShare, IconEdit, IconAlertSquareRounded, IconChevronLeft, IconChevronRight, IconFileAlert } from '@tabler/icons-react';
import { useResizeDetector } from 'react-resize-detector';
import DOMPurify from 'dompurify';
export function Message({ topic, card, profile, host }: { topic: Topic, card: Card | null, profile: Profile | null, host: boolean }) {
const { state, actions } = useMessage();
@ -21,6 +22,36 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
const textStyle = textColor && textSize ? { color: textColor, fontSize: textSize } : textColor ? { color: textColor } : textSize ? { fontSize: textSize } : {}
const logoUrl = profile ? profile.imageUrl : card ? card.imageUrl : avatar;
const timestamp = actions.getTimestamp(created);
const [message, setMessage] = useState(<p></p>);
useEffect(() => {
const urlPattern = new RegExp('(https?:\\/\\/)?(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)');
const hostPattern = new RegExp('^https?:\\/\\/', 'i');
let plain = '';
let clickable = [];
const parsed = !text ? '' : DOMPurify.sanitize(text).split(' ');
if (parsed?.length > 0) {
const words = parsed as string[];
words.forEach((word, index) => {
if (!!urlPattern.test(word)) {
clickable.push(<span key={index}>{ plain }</span>);
plain = '';
const url = !!hostPattern.test(word) ? word : `https://${word}`;
clickable.push(<a key={'link-'+index} target="_blank" rel="noopener noreferrer" href={url}>{ `${word} ` }</a>);
}
else {
plain += `${word} `;
}
})
}
if (plain) {
clickable.push(<span key={parsed.length}>{ plain }</span>);
}
setMessage(<span>{ clickable }</span>)
}, [text, locked]);
const [showScroll, setShowScroll] = useState(false);
const onResize = useCallback(() => {
@ -88,7 +119,7 @@ export function Message({ topic, card, profile, host }: { topic: Topic, card: Ca
</div>
{ !locked && status === 'confirmed' && text && (
<div style={textStyle}>
<span className={classes.text}>{ text }</span>
<span className={classes.text}>{ message }</span>
</div>
)}
{ !locked && status !== 'confirmed' && (

View File

@ -13,7 +13,7 @@ export function AudioAsset({ topicId, asset }: { topicId: string, asset: MediaAs
const { label } = asset.encrypted || asset.audio || { label: '' };
const [ loaded, setLoaded ] = useState(false);
const [ playing, setPlaying ] = useState(false);
const player = useRef();
const player = useRef(null as HTMLVideoElement | null);
const show = () => {
setShowModal(true);

View File

@ -23,7 +23,7 @@ export function useAudioAsset(topicId: string, asset: MediaAsset) {
},
loadAudio: async () => {
const { focus } = app.state;
const assetId = asset.audio ? asset.audio.hd : asset.encrypted ? asset.encrypted.parts : null;
const assetId = asset.audio ? asset.audio.full : asset.encrypted ? asset.encrypted.parts : null;
if (focus && assetId != null && !state.loading) {
updateState({ loading: true, loadPercent: 0 });
try {

View File

@ -7,7 +7,7 @@ import { MediaAsset } from '../../conversation/Conversation';
export function useBinaryAsset(topicId: string, asset: MediaAsset) {
const app = useContext(AppContext) as ContextType
const [state, setState] = useState({
dataUrl: null,
dataUrl: '',
loading: false,
loadPercent: 0,
})
@ -23,7 +23,7 @@ export function useBinaryAsset(topicId: string, asset: MediaAsset) {
},
loadBinary: async () => {
const { focus } = app.state;
const assetId = asset.audio ? asset.audio.hd : asset.encrypted ? asset.encrypted.parts : null;
const assetId = asset.binary ? asset.binary.data : asset.encrypted ? asset.encrypted.parts : null;
if (focus && assetId != null && !state.loading) {
updateState({ loading: true, loadPercent: 0 });
try {

View File

@ -1293,6 +1293,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304"
integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==
"@types/trusted-types@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/yargs-parser@*":
version "21.0.3"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15"
@ -2098,6 +2103,13 @@ domexception@^4.0.0:
dependencies:
webidl-conversions "^7.0.0"
dompurify@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.2.3.tgz#05dd2175225324daabfca6603055a09b2382a4cd"
integrity sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"