98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import clsx from "clsx";
|
|
import { t } from "i18next";
|
|
import { FC, useEffect, useRef, useState } from "react";
|
|
|
|
import { feedbackToast } from "@/utils/common";
|
|
|
|
import { IMessageItemProps } from ".";
|
|
import styles from "./message-item.module.scss";
|
|
|
|
const PLAYED_KEY = "chaglian_played_sounds";
|
|
|
|
const getPlayedMap = (): Record<string, boolean> => {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(PLAYED_KEY) ?? "{}");
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
const markPlayed = (clientMsgID: string) => {
|
|
const map = getPlayedMap();
|
|
map[clientMsgID] = true;
|
|
localStorage.setItem(PLAYED_KEY, JSON.stringify(map));
|
|
};
|
|
|
|
const SoundIcon = ({ className }: { className?: string }) => (
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" className={className}>
|
|
<path
|
|
d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5H4Z"
|
|
fill="currentColor"
|
|
/>
|
|
<path
|
|
d="M15 9.2a4 4 0 0 1 0 5.6M17.5 6.8a7.4 7.4 0 0 1 0 10.4"
|
|
stroke="currentColor"
|
|
strokeWidth="1.6"
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
);
|
|
|
|
const SoundMessageRender: FC<IMessageItemProps> = ({ message, isSender }) => {
|
|
const soundElem = message.soundElem;
|
|
const duration = Math.max(1, Math.round(soundElem?.duration ?? 0));
|
|
const [playing, setPlaying] = useState(false);
|
|
const [played, setPlayed] = useState(
|
|
() => Boolean(getPlayedMap()[message.clientMsgID]),
|
|
);
|
|
const audioRef = useRef<HTMLAudioElement>();
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
audioRef.current?.pause();
|
|
audioRef.current = undefined;
|
|
};
|
|
}, []);
|
|
|
|
// 宽度随时长微变:60~180px
|
|
const width = Math.min(180, 60 + duration * 6);
|
|
|
|
const play = () => {
|
|
const url = soundElem?.sourceUrl || soundElem?.soundPath;
|
|
if (!url || playing) return;
|
|
const audio = new Audio(url);
|
|
audioRef.current = audio;
|
|
setPlaying(true);
|
|
audio.onended = () => setPlaying(false);
|
|
audio.onerror = () => {
|
|
setPlaying(false);
|
|
feedbackToast({ msg: t("toast.accessFailed"), error: new Error("audio error") });
|
|
};
|
|
audio.play().catch(() => setPlaying(false));
|
|
markPlayed(message.clientMsgID);
|
|
setPlayed(true);
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center">
|
|
<div
|
|
className={clsx(
|
|
styles.bubble,
|
|
"flex cursor-pointer items-center justify-between text-sm",
|
|
isSender && "flex-row-reverse",
|
|
)}
|
|
style={{ width }}
|
|
onClick={play}
|
|
>
|
|
<SoundIcon className="shrink-0" />
|
|
<span className={clsx(isSender ? "mr-2" : "ml-2")}>{duration}″</span>
|
|
</div>
|
|
{!isSender && !played && (
|
|
<span className="ml-2 h-2 w-2 shrink-0 rounded-full bg-[var(--warn-text)]" />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SoundMessageRender;
|