From 96d4e7a437e582bac5c2260640b065527f821684 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 6 Feb 2024 16:48:24 +0100 Subject: [PATCH] Introduce one-time voice message (#4182) --- src/api/gramjs/apiBuilders/messageContent.ts | 23 ++- src/api/types/messages.ts | 2 + src/assets/font-icons/view-once.svg | 6 + src/assets/tgs/general/Flame.tgs | Bin 0 -> 4985 bytes src/bundles/extra.ts | 1 + src/components/common/Audio.scss | 121 +++++++++------ src/components/common/Audio.tsx | 141 ++++++++++++++---- .../common/helpers/animatedAssets.ts | 2 + .../helpers/renderActionMessageText.tsx | 6 + .../left/main/hooks/useChatListEntry.tsx | 10 ++ src/components/main/Main.tsx | 6 + src/components/middle/ActionMessage.tsx | 4 +- .../middle/message/ContextMenuContainer.tsx | 7 +- src/components/middle/message/Message.tsx | 8 +- .../oneTimeMedia/OneTimeMediaModal.async.tsx | 18 +++ .../OneTimeMediaModal.module.scss | 49 ++++++ .../modals/oneTimeMedia/OneTimeMediaModal.tsx | 91 +++++++++++ src/components/ui/ProgressSpinner.scss | 19 ++- src/components/ui/ProgressSpinner.tsx | 20 ++- src/config.ts | 2 +- src/global/actions/apiUpdaters/messages.ts | 4 + src/global/actions/ui/messages.ts | 18 +++ src/global/helpers/messageSummary.ts | 11 +- src/global/helpers/messages.ts | 22 ++- src/global/reducers/messages.ts | 10 ++ src/global/selectors/messages.ts | 14 +- src/global/types.ts | 7 + src/hooks/useAudioPlayer.ts | 20 ++- src/styles/icons.scss | 22 +-- src/styles/icons.woff | Bin 27740 -> 28060 bytes src/styles/icons.woff2 | Bin 23244 -> 23560 bytes src/types/icons/font.ts | 1 + src/types/index.ts | 1 + src/util/audioPlayer.ts | 2 +- src/util/handleError.ts | 17 ++- 35 files changed, 555 insertions(+), 130 deletions(-) create mode 100644 src/assets/font-icons/view-once.svg create mode 100644 src/assets/tgs/general/Flame.tgs create mode 100644 src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx create mode 100644 src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss create mode 100644 src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 1aead322e..5906e8ca7 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -72,7 +72,18 @@ export function buildMessageTextContent( } export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaContent | undefined { - if ('ttlSeconds' in media && media.ttlSeconds) { + const ttlSeconds = 'ttlSeconds' in media ? media.ttlSeconds : undefined; + + const isExpiredVoice = isExpiredVoiceMessage(media); + if (isExpiredVoice) { + return { isExpiredVoice }; + } + + const voice = buildVoice(media); + if (voice) return { voice, ttlSeconds }; + + // Other disappearing media types are not supported + if (ttlSeconds !== undefined) { return undefined; } @@ -93,9 +104,6 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC const audio = buildAudio(media); if (audio) return { audio }; - const voice = buildVoice(media); - if (voice) return { voice }; - const document = buildDocumentFromMedia(media); if (document) return { document }; @@ -255,6 +263,13 @@ function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined { }; } +function isExpiredVoiceMessage(media: GramJs.TypeMessageMedia): MediaContent['isExpiredVoice'] { + if (!(media instanceof GramJs.MessageMediaDocument)) { + return false; + } + return !media.document && media.voice; +} + function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined { if ( !(media instanceof GramJs.MessageMediaDocument) diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 4f7b87636..c02f0bcb5 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -475,6 +475,8 @@ export type MediaContent = { storyData?: ApiMessageStoryData; giveaway?: ApiGiveaway; giveawayResults?: ApiGiveawayResults; + ttlSeconds?: number; + isExpiredVoice?: boolean; }; export interface ApiMessage { diff --git a/src/assets/font-icons/view-once.svg b/src/assets/font-icons/view-once.svg new file mode 100644 index 000000000..7c58270b1 --- /dev/null +++ b/src/assets/font-icons/view-once.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/tgs/general/Flame.tgs b/src/assets/tgs/general/Flame.tgs new file mode 100644 index 0000000000000000000000000000000000000000..01661129813aa5be2425883bfb6037a07abaa77b GIT binary patch literal 4985 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 void; onPlay: (messageId: number, chatId: string) => void; + onPause?: NoneToVoidFunction; onReadMedia?: () => void; onCancelUpload?: () => void; onDateClick?: (messageId: number, chatId: string) => void; @@ -92,15 +98,23 @@ const Audio: FC = ({ isTranscriptionError, canDownload, canTranscribe, + autoPlay, onHideTranscription, onPlay, + onPause, onReadMedia, onCancelUpload, onDateClick, }) => { - const { cancelMessageMediaDownload, downloadMessageMedia, transcribeAudio } = getActions(); + const { + cancelMessageMediaDownload, downloadMessageMedia, transcribeAudio, openOneTimeMediaModal, + } = getActions(); - const { content: { audio, voice, video }, isMediaUnread } = message; + const { + content: { + audio, voice, video, + }, isMediaUnread, + } = message; const isVoice = Boolean(voice || video); const isSeeking = useRef(false); // eslint-disable-next-line no-null/no-null @@ -109,10 +123,13 @@ const Audio: FC = ({ const { isRtl } = lang; const { isMobile } = useAppLayout(); - const [isActivated, setIsActivated] = useState(false); + const [isActivated, setIsActivated] = useState(Boolean(autoPlay)); const shouldLoad = isActivated || PRELOAD; const coverHash = getMessageMediaHash(message, 'pictogram'); const coverBlobUrl = useMedia(coverHash, false, ApiMediaFormat.BlobUrl); + const hasTtl = hasMessageTtl(message); + const isOneTimeModalOrigin = origin === AudioOrigin.OneTimeModal; + const trackType = isVoice ? (hasTtl ? 'oneTimeVoice' : 'voice') : 'audio'; const mediaData = useMedia( getMessageMediaHash(message, 'inline'), @@ -139,12 +156,13 @@ const Audio: FC = ({ isBuffered, bufferedRanges, bufferingHandlers, checkBuffering, } = useBuffering(); + const noReset = isOneTimeModalOrigin; const { isPlaying, playProgress, playPause, setCurrentTime, duration, } = useAudioPlayer( makeTrackId(message), getMediaDuration(message)!, - isVoice ? 'voice' : 'audio', + trackType, mediaData, bufferingHandlers, undefined, @@ -152,12 +170,24 @@ const Audio: FC = ({ isActivated, handleForcePlay, handleTrackChange, - isMessageLocal(message), + isMessageLocal(message) || hasTtl, + undefined, + onPause, + noReset, ); + const reversePlayProgress = 1 - playProgress; const isOwn = isOwnMessage(message); + const isReverse = hasTtl && isOneTimeModalOrigin; + const waveformCanvasRef = useWaveformCanvas( - theme, voice, (isMediaUnread && !isOwn) ? 1 : playProgress, isOwn, !noAvatars, isMobile, + theme, + voice, + (isMediaUnread && !isOwn && !isReverse) ? 1 : playProgress, + isOwn, + !noAvatars, + isMobile, + isReverse, ); const withSeekline = isPlaying || (playProgress > 0 && playProgress < 1); @@ -189,6 +219,13 @@ const Audio: FC = ({ return; } + if (hasTtl) { + // Set new date to prevent saving state of the track + openOneTimeMediaModal({ message: { ...message, date: Date.now() } }); + onReadMedia?.(); + return; + } + if (!isPlaying) { onPlay(message.id, message.chatId); } @@ -241,14 +278,14 @@ const Audio: FC = ({ }); useEffect(() => { - if (!seekerRef.current || !withSeekline) return undefined; + if (!seekerRef.current || !withSeekline || isOneTimeModalOrigin) return undefined; return captureEvents(seekerRef.current, { onCapture: handleStartSeek, onRelease: handleStopSeek, onClick: handleStopSeek, onDrag: handleSeek, }); - }, [withSeekline, handleStartSeek, handleSeek, handleStopSeek]); + }, [withSeekline, handleStartSeek, handleSeek, handleStopSeek, isOneTimeModalOrigin]); function renderFirstLine() { if (isVoice) { @@ -285,6 +322,7 @@ const Audio: FC = ({ const fullClassName = buildClassName( 'Audio', className, + isOneTimeModalOrigin && 'non-interactive', origin === AudioOrigin.Inline && 'inline', isOwn && origin === AudioOrigin.Inline && 'own', (origin === AudioOrigin.Search || origin === AudioOrigin.SharedMedia) && 'bigger', @@ -331,6 +369,40 @@ const Audio: FC = ({ ); } + function renderTooglePlayWrapper() { + return ( +
+ + {hasTtl && !isOneTimeModalOrigin && ( + + )} +
+ ); + } + return (
{isSelectable && ( @@ -338,31 +410,30 @@ const Audio: FC = ({ {isSelected && }
)} - + {renderTooglePlayWrapper()} {shouldRenderSpinner && (
)} + {isOneTimeModalOrigin && !shouldRenderSpinner && ( +
+ +
+ )} {audio && canDownload && !isUploading && ( )} -

- {playProgress === 0 ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)} +

+ {playProgress === 0 || playProgress === 1 + ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}

); @@ -551,6 +628,7 @@ function useWaveformCanvas( isOwn = false, withAvatar = false, isMobile = false, + isReverse = false, ) { // eslint-disable-next-line no-null/no-null const canvasRef = useRef(null); @@ -588,12 +666,15 @@ function useWaveformCanvas( const progressFillColor = theme === 'dark' ? '#8774E1' : '#3390EC'; const progressFillOwnColor = theme === 'dark' ? '#FFFFFF' : '#4FAE4E'; - renderWaveform(canvas, spikes, playProgress, { + const fillStyle = isOwn ? fillOwnColor : fillColor; + const progressFillStyle = isOwn ? progressFillOwnColor : progressFillColor; + + renderWaveform(canvas, spikes, isReverse ? 1 - playProgress : playProgress, { peak, - fillStyle: isOwn ? fillOwnColor : fillColor, - progressFillStyle: isOwn ? progressFillOwnColor : progressFillColor, + fillStyle, + progressFillStyle, }); - }, [isOwn, peak, playProgress, spikes, theme]); + }, [isOwn, peak, playProgress, spikes, theme, isReverse]); return canvasRef; } diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index a1fac8420..54350bfba 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -7,6 +7,7 @@ import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs'; import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs'; import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; +import Flame from '../../../assets/tgs/general/Flame.tgs'; import PartyPopper from '../../../assets/tgs/general/PartyPopper.tgs'; import Invite from '../../../assets/tgs/invites/Invite.tgs'; import JoinRequest from '../../../assets/tgs/invites/Requests.tgs'; @@ -46,4 +47,5 @@ export const LOCAL_TGS_URLS = { Congratulations, Experimental, PartyPopper, + Flame, }; diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index d34ffb56b..0ea61633c 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -9,8 +9,10 @@ import type { TextPart } from '../../../types'; import { getChatTitle, + getExpiredMessageDescription, getMessageSummaryText, getUserFullName, + isExpiredMessage, } from '../../../global/helpers'; import { formatCurrency } from '../../../util/formatCurrency'; import trimText from '../../../util/trimText'; @@ -45,6 +47,10 @@ export function renderActionMessageText( observeIntersectionForLoading?: ObserveFn, observeIntersectionForPlaying?: ObserveFn, ) { + if (isExpiredMessage(message)) { + return getExpiredMessageDescription(lang, message); + } + if (!message.content.action) { return []; } diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 5caf68235..03e9be663 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -13,6 +13,7 @@ import type { LangFn } from '../../../../hooks/useLang'; import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config'; import { requestMutation } from '../../../../lib/fasterdom/fasterdom'; import { + getExpiredMessageDescription, getMessageIsSpoiler, getMessageMediaHash, getMessageMediaThumbDataUri, @@ -22,6 +23,7 @@ import { getMessageVideo, isActionMessage, isChatChannel, + isExpiredMessage, } from '../../../../global/helpers'; import { getMessageReplyInfo } from '../../../../global/helpers/replies'; import buildClassName from '../../../../util/buildClassName'; @@ -127,6 +129,14 @@ export default function useChatListEntry({ return undefined; } + if (isExpiredMessage(lastMessage)) { + return ( +

+ {getExpiredMessageDescription(lang, lastMessage)} +

+ ); + } + if (isAction) { const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 5fd7a821f..9fb1ab657 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -80,6 +80,7 @@ import BoostModal from '../modals/boost/BoostModal.async'; import ChatlistModal from '../modals/chatlist/ChatlistModal.async'; import GiftCodeModal from '../modals/giftcode/GiftCodeModal.async'; import MapModal from '../modals/map/MapModal.async'; +import OneTimeMediaModal from '../modals/oneTimeMedia/OneTimeMediaModal.async'; import UrlAuthModal from '../modals/urlAuth/UrlAuthModal.async'; import WebAppModal from '../modals/webApp/WebAppModal.async'; import PaymentModal from '../payment/PaymentModal.async'; @@ -163,6 +164,7 @@ type StateProps = { withInterfaceAnimations?: boolean; isSynced?: boolean; inviteViaLinkModal?: TabState['inviteViaLinkModal']; + oneTimeMediaModal?: TabState['oneTimeMediaModal']; }; const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min @@ -224,6 +226,7 @@ const Main: FC = ({ noRightColumnAnimation, isSynced, inviteViaLinkModal, + oneTimeMediaModal, }) => { const { initMain, @@ -568,6 +571,7 @@ const Main: FC = ({ /> + @@ -635,6 +639,7 @@ export default memo(withGlobal( boostModal, giftCodeModal, inviteViaLinkModal, + oneTimeMediaModal, } = selectTabState(global); const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer; @@ -703,6 +708,7 @@ export default memo(withGlobal( noRightColumnAnimation, isSynced: global.isSynced, inviteViaLinkModal, + oneTimeMediaModal, }; }, )(Main)); diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index 045e50524..3428951fc 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -12,7 +12,9 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { FocusDirection } from '../../types'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; -import { getChatTitle, getMessageHtmlId, isChatChannel } from '../../global/helpers'; +import { + getChatTitle, getMessageHtmlId, isChatChannel, +} from '../../global/helpers'; import { getMessageReplyInfo } from '../../global/helpers/replies'; import { selectCanPlayAnimatedEmojis, diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 62d529f8f..8f09e5fac 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -14,6 +14,7 @@ import { PREVIEW_AVATAR_COUNT, SERVICE_NOTIFICATIONS_USER_ID } from '../../../co import { areReactionsEmpty, getMessageVideo, + hasMessageTtl, isActionMessage, isChatChannel, isChatGroup, @@ -649,6 +650,7 @@ export default memo(withGlobal( const isScheduled = messageListType === 'scheduled'; const isChannel = chat && isChatChannel(chat); const isLocal = isMessageLocal(message); + const hasTtl = hasMessageTtl(message); const canShowSeenBy = Boolean(!isLocal && chat && seenByMaxChatMembers @@ -695,7 +697,7 @@ export default memo(withGlobal( canForward: !isScheduled && canForward, canFaveSticker: !isScheduled && canFaveSticker, canUnfaveSticker: !isScheduled && canUnfaveSticker, - canCopy: canCopyNumber || (!isProtected && canCopy), + canCopy: (canCopyNumber || (!isProtected && canCopy)), canCopyLink: !isScheduled && canCopyLink, canSelect, canDownload: !isProtected && canDownload, @@ -710,7 +712,8 @@ export default memo(withGlobal( isCurrentUserPremium, hasFullInfo: Boolean(chatFullInfo), canShowReactionsCount, - canShowReactionList: !isLocal && !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID, + canShowReactionList: !isLocal && !isAction + && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID && !hasTtl, canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global), customEmojiSetsInfo, customEmojiSets, diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index de860d127..499dccebb 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -40,6 +40,7 @@ import { getMessageSingleRegularEmoji, getSenderTitle, hasMessageText, + hasMessageTtl, isAnonymousOwnMessage, isChatChannel, isChatGroup, @@ -524,6 +525,7 @@ const Message: FC = ({ const messageColorPeer = originSender || sender; const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender; const hasText = hasMessageText(message); + const hasTtl = hasMessageTtl(message); const { handleMouseDown, @@ -671,7 +673,7 @@ const Message: FC = ({ && !isInDocumentGroupNotLast && messageListType === 'thread' && !noComments; const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction - && !isInDocumentGroupNotLast && !isStoryMention; + && !isInDocumentGroupNotLast && !isStoryMention && !hasTtl; const contentClassName = buildContentClassName(message, { hasSubheader, @@ -1107,7 +1109,7 @@ const Message: FC = ({ isSelected={isSelected} noAvatars={noAvatars} onPlay={handleAudioPlay} - onReadMedia={voice && (!isOwn || isChatWithSelf) ? handleReadMedia : undefined} + onReadMedia={voice && (!isOwn || isChatWithSelf || (isOwn && !hasTtl)) ? handleReadMedia : undefined} onCancelUpload={handleCancelUpload} isDownloading={isDownloading} isTranscribing={isTranscribing} @@ -1116,7 +1118,7 @@ const Message: FC = ({ isTranscriptionError={isTranscriptionError} canDownload={!isProtected} onHideTranscription={setTranscriptionHidden} - canTranscribe={isPremium} + canTranscribe={isPremium && !hasTtl} /> )} {document && ( diff --git a/src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx b/src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx new file mode 100644 index 000000000..6edd5b050 --- /dev/null +++ b/src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './OneTimeMediaModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const OneTimeMediaModalAsync: FC = (props) => { + const { info } = props; + const OneTimeMediaModal = useModuleLoader(Bundles.Extra, 'OneTimeMediaModal', !info); + + // eslint-disable-next-line react/jsx-props-no-spreading + return OneTimeMediaModal ? : undefined; +}; + +export default OneTimeMediaModalAsync; diff --git a/src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss b/src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss new file mode 100644 index 000000000..002e2863f --- /dev/null +++ b/src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss @@ -0,0 +1,49 @@ + +.root { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + flex-direction: column; + backdrop-filter: blur(2rem); + animation: fade-in-opacity 0.3s ease; + background-color: rgba(0, 0, 0, 0.25); + z-index: var(--z-modal-confirm); + align-items: center; + transition: opacity 0.3s ease; + + &.closing { + opacity: 0; + } +} + +.main { + background-color: var(--color-background); + padding: 0.6875rem; + border-radius: 1rem; +} + +.footer { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + margin-bottom: 2rem; +} + +.closeBtn { + margin: 0 auto; + width: auto; +} + +@keyframes fade-in-opacity { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx b/src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx new file mode 100644 index 000000000..20fd968af --- /dev/null +++ b/src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx @@ -0,0 +1,91 @@ +import React, { memo } from '../../../lib/teact/teact'; +import { getActions, getGlobal } from '../../../global'; + +import type { TabState } from '../../../global/types'; +import { AudioOrigin } from '../../../types'; + +import { isOwnMessage } from '../../../global/helpers'; +import { selectTheme } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; + +import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useShowTransition from '../../../hooks/useShowTransition'; + +import Audio from '../../common/Audio'; +import Button from '../../ui/Button'; + +import styles from './OneTimeMediaModal.module.scss'; + +export type OwnProps = { + info: TabState['oneTimeMediaModal']; +}; + +const OneTimeMediaModal = ({ + info, +}: OwnProps) => { + const { + closeOneTimeMediaModal, + } = getActions(); + + const lang = useLang(); + const message = useCurrentOrPrev(info?.message, true); + + const { + shouldRender, + transitionClassNames, + } = useShowTransition(Boolean(info)); + + const handlePlayVoice = useLastCallback(() => { + return undefined; + }); + + const handleClose = useLastCallback(() => { + closeOneTimeMediaModal(); + }); + + if (!shouldRender || !message) { + return undefined; + } + + const isOwn = isOwnMessage(message); + const theme = selectTheme(getGlobal()); + const closeBtnTitle = isOwn ? lang('Chat.Voice.Single.Close') : lang('Chat.Voice.Single.DeleteAndClose'); + + function renderMedia() { + if (message?.content?.voice) { + return ( +