One-Time Video: Show and play one-time video (#4258)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
parent
80e70d2899
commit
cb9330bcef
@ -78,10 +78,19 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC
|
||||
if (isExpiredVoice) {
|
||||
return { isExpiredVoice };
|
||||
}
|
||||
const isExpiredRoundVideo = isExpiredRoundVideoMessage(media);
|
||||
if (isExpiredRoundVideo) {
|
||||
return { isExpiredRoundVideo };
|
||||
}
|
||||
|
||||
const voice = buildVoice(media);
|
||||
if (voice) return { voice, ttlSeconds };
|
||||
|
||||
if ('round' in media && media.round) {
|
||||
const video = buildVideo(media);
|
||||
if (video) return { video, ttlSeconds };
|
||||
}
|
||||
|
||||
// Other disappearing media types are not supported
|
||||
if (ttlSeconds !== undefined) {
|
||||
return undefined;
|
||||
@ -270,6 +279,13 @@ function isExpiredVoiceMessage(media: GramJs.TypeMessageMedia): MediaContent['is
|
||||
return !media.document && media.voice;
|
||||
}
|
||||
|
||||
function isExpiredRoundVideoMessage(media: GramJs.TypeMessageMedia): MediaContent['isExpiredRoundVideo'] {
|
||||
if (!(media instanceof GramJs.MessageMediaDocument)) {
|
||||
return false;
|
||||
}
|
||||
return !media.document && media.round;
|
||||
}
|
||||
|
||||
function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined {
|
||||
if (
|
||||
!(media instanceof GramJs.MessageMediaDocument)
|
||||
|
||||
@ -485,8 +485,9 @@ export type MediaContent = {
|
||||
storyData?: ApiMessageStoryData;
|
||||
giveaway?: ApiGiveaway;
|
||||
giveawayResults?: ApiGiveawayResults;
|
||||
ttlSeconds?: number;
|
||||
isExpiredVoice?: boolean;
|
||||
isExpiredRoundVideo?: boolean;
|
||||
ttlSeconds?: number;
|
||||
};
|
||||
|
||||
export interface ApiMessage {
|
||||
|
||||
@ -128,7 +128,7 @@ const Audio: FC<OwnProps> = ({
|
||||
const coverHash = getMessageMediaHash(message, 'pictogram');
|
||||
const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl);
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
const isOneTimeModalOrigin = origin === AudioOrigin.OneTimeModal;
|
||||
const isInOneTimeModal = origin === AudioOrigin.OneTimeModal;
|
||||
const trackType = isVoice ? (hasTtl ? 'oneTimeVoice' : 'voice') : 'audio';
|
||||
|
||||
const mediaData = useMedia(
|
||||
@ -156,7 +156,7 @@ const Audio: FC<OwnProps> = ({
|
||||
isBuffered, bufferedRanges, bufferingHandlers, checkBuffering,
|
||||
} = useBuffering();
|
||||
|
||||
const noReset = isOneTimeModalOrigin;
|
||||
const noReset = isInOneTimeModal;
|
||||
const {
|
||||
isPlaying, playProgress, playPause, setCurrentTime, duration,
|
||||
} = useAudioPlayer(
|
||||
@ -179,7 +179,7 @@ const Audio: FC<OwnProps> = ({
|
||||
|
||||
const reversePlayProgress = 1 - playProgress;
|
||||
const isOwn = isOwnMessage(message);
|
||||
const isReverse = hasTtl && isOneTimeModalOrigin;
|
||||
const isReverse = hasTtl && isInOneTimeModal;
|
||||
|
||||
const waveformCanvasRef = useWaveformCanvas(
|
||||
theme,
|
||||
@ -279,14 +279,14 @@ const Audio: FC<OwnProps> = ({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!seekerRef.current || !withSeekline || isOneTimeModalOrigin) return undefined;
|
||||
if (!seekerRef.current || !withSeekline || isInOneTimeModal) return undefined;
|
||||
return captureEvents(seekerRef.current, {
|
||||
onCapture: handleStartSeek,
|
||||
onRelease: handleStopSeek,
|
||||
onClick: handleStopSeek,
|
||||
onDrag: handleSeek,
|
||||
});
|
||||
}, [withSeekline, handleStartSeek, handleSeek, handleStopSeek, isOneTimeModalOrigin]);
|
||||
}, [withSeekline, handleStartSeek, handleSeek, handleStopSeek, isInOneTimeModal]);
|
||||
|
||||
function renderFirstLine() {
|
||||
if (isVoice) {
|
||||
@ -323,7 +323,7 @@ const Audio: FC<OwnProps> = ({
|
||||
const fullClassName = buildClassName(
|
||||
'Audio',
|
||||
className,
|
||||
isOneTimeModalOrigin && 'non-interactive',
|
||||
isInOneTimeModal && 'non-interactive',
|
||||
origin === AudioOrigin.Inline && 'inline',
|
||||
isOwn && origin === AudioOrigin.Inline && 'own',
|
||||
(origin === AudioOrigin.Search || origin === AudioOrigin.SharedMedia) && 'bigger',
|
||||
@ -383,11 +383,11 @@ const Audio: FC<OwnProps> = ({
|
||||
onClick={handleButtonClick}
|
||||
isRtl={lang.isRtl}
|
||||
backgroundImage={coverBlobUrl}
|
||||
nonInteractive={isOneTimeModalOrigin}
|
||||
nonInteractive={isInOneTimeModal}
|
||||
>
|
||||
{!isOneTimeModalOrigin && <Icon name="play" />}
|
||||
{!isOneTimeModalOrigin && <Icon name="pause" />}
|
||||
{isOneTimeModalOrigin && (
|
||||
{!isInOneTimeModal && <Icon name="play" />}
|
||||
{!isInOneTimeModal && <Icon name="pause" />}
|
||||
{isInOneTimeModal && (
|
||||
<AnimatedIcon
|
||||
className="flame"
|
||||
tgsUrl={LOCAL_TGS_URLS.Flame}
|
||||
@ -397,7 +397,7 @@ const Audio: FC<OwnProps> = ({
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
{hasTtl && !isOneTimeModalOrigin && (
|
||||
{hasTtl && !isInOneTimeModal && (
|
||||
<Icon name="view-once" />
|
||||
)}
|
||||
</div>
|
||||
@ -424,7 +424,7 @@ const Audio: FC<OwnProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isOneTimeModalOrigin && !shouldRenderSpinner && (
|
||||
{isInOneTimeModal && !shouldRenderSpinner && (
|
||||
<div className={buildClassName('media-loading')}>
|
||||
<ProgressSpinner
|
||||
progress={playProgress}
|
||||
@ -461,7 +461,7 @@ const Audio: FC<OwnProps> = ({
|
||||
onDateClick ? handleDateClick : undefined,
|
||||
)}
|
||||
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
|
||||
{(origin === AudioOrigin.Inline || isOneTimeModalOrigin) && voice && (
|
||||
{(origin === AudioOrigin.Inline || isInOneTimeModal) && voice && (
|
||||
renderVoice(
|
||||
voice,
|
||||
seekerRef,
|
||||
|
||||
@ -964,6 +964,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
metaPosition === 'in-text' && 'with-meta',
|
||||
outgoingStatus && 'with-outgoing-icon',
|
||||
);
|
||||
const shouldReadMedia = !hasTtl || !isOwn || isChatWithSelf;
|
||||
|
||||
return (
|
||||
<div className={className} onDoubleClick={handleContentDoubleClick} dir="auto">
|
||||
@ -1086,6 +1087,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
isDownloading={isDownloading}
|
||||
onReadMedia={shouldReadMedia ? handleReadMedia : undefined}
|
||||
/>
|
||||
)}
|
||||
{!isAlbum && video && !video.isRound && (
|
||||
@ -1115,7 +1117,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isSelected={isSelected}
|
||||
noAvatars={noAvatars}
|
||||
onPlay={handleAudioPlay}
|
||||
onReadMedia={voice && (!isOwn || isChatWithSelf || (isOwn && !hasTtl)) ? handleReadMedia : undefined}
|
||||
onReadMedia={voice && shouldReadMedia ? handleReadMedia : undefined}
|
||||
onCancelUpload={handleCancelUpload}
|
||||
isDownloading={isDownloading}
|
||||
isTranscribing={isTranscribing}
|
||||
|
||||
@ -4,15 +4,24 @@
|
||||
height: 15rem;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
&.non-interactive {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
canvas {
|
||||
.thumbnail {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@ -38,4 +47,24 @@
|
||||
video::-webkit-media-controls-start-playback-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.play-wrapper {
|
||||
position: absolute;
|
||||
|
||||
.icon-play {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.icon-view-once {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
padding: 0.125rem;
|
||||
left: 1.625rem;
|
||||
bottom: 0;
|
||||
font-size: 1rem;
|
||||
border-radius: 50%;
|
||||
color: var(--color-white);
|
||||
z-index: var(--z-badge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,9 @@ import type { ApiMessage } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri } from '../../../global/helpers';
|
||||
import {
|
||||
getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri, hasMessageTtl,
|
||||
} from '../../../global/helpers';
|
||||
import { stopCurrentAudio } from '../../../util/audioPlayer';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||
@ -29,6 +31,9 @@ import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useSignal from '../../../hooks/useSignal';
|
||||
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
||||
|
||||
import Icon from '../../common/Icon';
|
||||
import MediaSpoiler from '../../common/MediaSpoiler';
|
||||
import Button from '../../ui/Button';
|
||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
import ProgressSpinner from '../../ui/ProgressSpinner';
|
||||
|
||||
@ -36,9 +41,13 @@ import './RoundVideo.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
observeIntersection: ObserveFn;
|
||||
className?: string;
|
||||
canAutoLoad?: boolean;
|
||||
isDownloading?: boolean;
|
||||
origin?: 'oneTimeModal';
|
||||
observeIntersection?: ObserveFn;
|
||||
onStop?: NoneToVoidFunction;
|
||||
onReadMedia?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const PROGRESS_CENTER = ROUND_VIDEO_DIMENSIONS_PX / 2;
|
||||
@ -50,9 +59,13 @@ let stopPrevious: NoneToVoidFunction;
|
||||
|
||||
const RoundVideo: FC<OwnProps> = ({
|
||||
message,
|
||||
observeIntersection,
|
||||
className,
|
||||
canAutoLoad,
|
||||
isDownloading,
|
||||
origin,
|
||||
observeIntersection,
|
||||
onStop,
|
||||
onReadMedia,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -63,6 +76,8 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
|
||||
const video = message.content.video!;
|
||||
|
||||
const { cancelMessageMediaDownload, openOneTimeMediaModal } = getActions();
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
|
||||
@ -80,11 +95,14 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
);
|
||||
|
||||
const [isPlayerReady, markPlayerReady] = useFlag();
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
const isInOneTimeModal = origin === 'oneTimeModal';
|
||||
const shouldRenderSpoiler = hasTtl && !isInOneTimeModal;
|
||||
const hasThumb = Boolean(getMessageMediaThumbDataUri(message));
|
||||
const noThumb = !hasThumb || isPlayerReady;
|
||||
const noThumb = !hasThumb || isPlayerReady || shouldRenderSpoiler;
|
||||
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
|
||||
const thumbDataUri = getMessageMediaThumbDataUri(message);
|
||||
const isTransferring = (isLoadAllowed && !isPlayerReady) || isDownloading;
|
||||
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||
|
||||
@ -133,18 +151,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
stopPrevious = stopPlaying;
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (!mediaData) {
|
||||
setIsLoadAllowed((isAllowed) => !isAllowed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
getActions().cancelMessageMediaDownload({ message });
|
||||
return;
|
||||
}
|
||||
|
||||
const tooglePlaying = useLastCallback(() => {
|
||||
const playerEl = playerRef.current!;
|
||||
if (isActivated) {
|
||||
if (playerEl.paused) {
|
||||
@ -160,25 +167,77 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
playerEl.currentTime = 0;
|
||||
safePlay(playerEl);
|
||||
stopCurrentAudio();
|
||||
|
||||
setIsActivated(true);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInOneTimeModal) {
|
||||
return;
|
||||
}
|
||||
tooglePlaying();
|
||||
}, [isInOneTimeModal]);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (!mediaData) {
|
||||
setIsLoadAllowed((isAllowed) => !isAllowed);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownloading) {
|
||||
cancelMessageMediaDownload({ message });
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasTtl && !isInOneTimeModal) {
|
||||
openOneTimeMediaModal({ message });
|
||||
onReadMedia?.();
|
||||
return;
|
||||
}
|
||||
|
||||
tooglePlaying();
|
||||
});
|
||||
|
||||
const handleTimeUpdate = useLastCallback((e: React.UIEvent<HTMLVideoElement>) => {
|
||||
const playerEl = e.currentTarget;
|
||||
|
||||
setProgress(playerEl.currentTime / playerEl.duration);
|
||||
});
|
||||
|
||||
function renderPlayWrapper() {
|
||||
return (
|
||||
<div className="play-wrapper">
|
||||
<Button
|
||||
color="dark"
|
||||
round
|
||||
size="smaller"
|
||||
className="play"
|
||||
nonInteractive
|
||||
>
|
||||
<Icon name="play" />
|
||||
</Button>
|
||||
<Icon name="view-once" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="RoundVideo media-inner"
|
||||
className={buildClassName('RoundVideo', 'media-inner', isInOneTimeModal && 'non-interactive', className)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{mediaData && (
|
||||
<div className="video-wrapper">
|
||||
{shouldRenderSpoiler && (
|
||||
<MediaSpoiler
|
||||
isVisible
|
||||
thumbDataUri={thumbDataUri}
|
||||
width={ROUND_VIDEO_DIMENSIONS_PX}
|
||||
height={ROUND_VIDEO_DIMENSIONS_PX}
|
||||
className="media-spoiler"
|
||||
/>
|
||||
)}
|
||||
<OptimizedVideo
|
||||
canPlay={shouldPlay}
|
||||
ref={playerRef}
|
||||
@ -186,22 +245,24 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
className="full-media"
|
||||
width={ROUND_VIDEO_DIMENSIONS_PX}
|
||||
height={ROUND_VIDEO_DIMENSIONS_PX}
|
||||
autoPlay
|
||||
autoPlay={!shouldRenderSpoiler}
|
||||
disablePictureInPicture
|
||||
muted={!isActivated}
|
||||
loop={!isActivated}
|
||||
playsInline
|
||||
onEnded={isActivated ? stopPlaying : undefined}
|
||||
onEnded={isActivated ? onStop ?? stopPlaying : undefined}
|
||||
onTimeUpdate={isActivated ? handleTimeUpdate : undefined}
|
||||
onReady={markPlayerReady}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
|
||||
/>
|
||||
{!shouldRenderSpoiler && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
|
||||
/>
|
||||
)}
|
||||
<div className="progress">
|
||||
{isActivated && (
|
||||
<svg width={ROUND_VIDEO_DIMENSIONS_PX} height={ROUND_VIDEO_DIMENSIONS_PX}>
|
||||
@ -223,13 +284,16 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
<ProgressSpinner progress={isDownloading ? downloadProgress : loadProgress} />
|
||||
</div>
|
||||
)}
|
||||
{shouldRenderSpoiler && !shouldSpinnerRender && renderPlayWrapper()}
|
||||
{!mediaData && !isLoadAllowed && (
|
||||
<i className="icon icon-download" />
|
||||
)}
|
||||
<div className="message-media-duration">
|
||||
{isActivated ? formatMediaDuration(playerRef.current!.currentTime) : formatMediaDuration(video.duration)}
|
||||
{(!isActivated || playerRef.current!.paused) && <i className="icon icon-muted" />}
|
||||
</div>
|
||||
{!isInOneTimeModal && (
|
||||
<div className="message-media-duration">
|
||||
{isActivated ? formatMediaDuration(playerRef.current!.currentTime) : formatMediaDuration(video.duration)}
|
||||
{(!isActivated || playerRef.current!.paused) && <Icon name="muted" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(2rem);
|
||||
animation: fade-in-opacity 0.3s ease;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: var(--z-modal-confirm);
|
||||
align-items: center;
|
||||
transition: opacity 0.3s ease;
|
||||
@ -20,10 +20,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
background-color: var(--color-background);
|
||||
.voice {
|
||||
padding: 0.6875rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.video {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
||||
@ -14,6 +14,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
import Audio from '../../common/Audio';
|
||||
import RoundVideo from '../../middle/message/RoundVideo';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import styles from './OneTimeMediaModal.module.scss';
|
||||
@ -54,9 +55,14 @@ const OneTimeMediaModal = ({
|
||||
const closeBtnTitle = isOwn ? lang('Chat.Voice.Single.Close') : lang('Chat.Voice.Single.DeleteAndClose');
|
||||
|
||||
function renderMedia() {
|
||||
if (message?.content?.voice) {
|
||||
if (!message?.content) {
|
||||
return undefined;
|
||||
}
|
||||
const { voice, video } = message.content;
|
||||
if (voice) {
|
||||
return (
|
||||
<Audio
|
||||
className={styles.voice}
|
||||
theme={theme}
|
||||
message={message}
|
||||
origin={AudioOrigin.OneTimeModal}
|
||||
@ -65,13 +71,22 @@ const OneTimeMediaModal = ({
|
||||
onPause={handleClose}
|
||||
/>
|
||||
);
|
||||
} else if (video?.isRound) {
|
||||
return (
|
||||
<RoundVideo
|
||||
className={styles.video}
|
||||
message={message}
|
||||
origin="oneTimeModal"
|
||||
onStop={handleClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, transitionClassNames)}>
|
||||
<div className={styles.main}>{renderMedia()}</div>
|
||||
{renderMedia()}
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
faded
|
||||
|
||||
@ -324,15 +324,18 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList =
|
||||
}
|
||||
|
||||
export function getExpiredMessageDescription(langFn: LangFn, message: ApiMessage): string | undefined {
|
||||
const { isExpiredVoice } = message.content;
|
||||
const { isExpiredVoice, isExpiredRoundVideo } = message.content;
|
||||
if (isExpiredVoice) {
|
||||
return langFn('Message.VoiceMessageExpired');
|
||||
} else if (isExpiredRoundVideo) {
|
||||
return langFn('Message.VideoMessageExpired');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isExpiredMessage(message: ApiMessage) {
|
||||
return Boolean(message.content?.isExpiredVoice);
|
||||
const { isExpiredVoice, isExpiredRoundVideo } = message.content ?? {};
|
||||
return Boolean(isExpiredVoice || isExpiredRoundVideo);
|
||||
}
|
||||
|
||||
export function hasMessageTtl(message: ApiMessage) {
|
||||
|
||||
@ -206,6 +206,12 @@ export function updateChatMessage<T extends GlobalState>(
|
||||
voice: undefined,
|
||||
isExpiredVoice: true,
|
||||
};
|
||||
} else if (message.content.video?.isRound) {
|
||||
messageUpdate.content = {
|
||||
...messageUpdate.content,
|
||||
video: undefined,
|
||||
isExpiredRoundVideo: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
const updatedMessage = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user