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 000000000..016611298 Binary files /dev/null and b/src/assets/tgs/general/Flame.tgs differ diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 0dcc5e7cc..52652ded5 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -83,3 +83,4 @@ export { default as Management } from '../components/right/management/Management export { default as PaymentModal } from '../components/payment/PaymentModal'; export { default as ReceiptModal } from '../components/payment/ReceiptModal'; export { default as InviteViaLinkModal } from '../components/main/InviteViaLinkModal'; +export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/OneTimeMediaModal'; diff --git a/src/components/common/Audio.scss b/src/components/common/Audio.scss index 9731eab77..155092941 100644 --- a/src/components/common/Audio.scss +++ b/src/components/common/Audio.scss @@ -24,6 +24,72 @@ } } + .toogle-play-wrapper { + margin: 0; + + .toggle-play { + margin-inline-end: 0.5rem; + + &.translucent-white { + color: rgba(255, 255, 255, 0.8); + } + + &.smaller { + width: 3rem; + height: 3rem; + margin-inline-end: 0.75rem; + + .icon { + font-size: 1.625rem; + + &.icon-pause { + font-size: 1.5625rem; + } + } + } + + .icon { + position: absolute; + + &.icon-play { + margin-left: 0.1875rem; + @media (max-width: 600px) { + margin-left: 0.125rem; + } + } + } + + .icon-play, + .icon-pause, + .flame { + opacity: 1; + transform: scale(1); + transition: opacity 0.4s, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + &.play .icon-pause, + &.pause .icon-play, + &.loading .icon-play, + &.loading .icon-pause, + &.loading .flame { + opacity: 0; + transform: scale(0.5); + } + } + + .icon-view-once { + position: absolute; + left: 2rem; + bottom: 0rem; + font-size: 1.1875rem; + border-radius: 50%; + color: var(--color-white); + background-color: var(--color-primary); + outline: var(--background-color) solid 0.125rem; + z-index: var(--z-badge); + } + } + &.own { --color-text-secondary: var(--accent-color); --color-interactive-active: var(--color-text-green); @@ -35,7 +101,7 @@ --color-text-green: var(--color-white); } - .Button { + .Button, .icon-view-once, .media-loading { --color-primary: var(--color-text-green); --color-primary-shade: var(--color-green); --color-primary-shade-darker: var(--color-green-darker); @@ -48,54 +114,6 @@ } } - .toggle-play { - margin-inline-end: 0.5rem; - - &.translucent-white { - color: rgba(255, 255, 255, 0.8); - } - - &.smaller { - width: 3rem; - height: 3rem; - margin-inline-end: 0.75rem; - - .icon { - font-size: 1.625rem; - - &.icon-pause { - font-size: 1.5625rem; - } - } - } - - .icon { - position: absolute; - - &.icon-play { - margin-left: 0.1875rem; - @media (max-width: 600px) { - margin-left: 0.125rem; - } - } - } - - .icon-play, - .icon-pause { - opacity: 1; - transform: scale(1); - transition: opacity 0.4s, transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); - } - - &.play .icon-pause, - &.pause .icon-play, - &.loading .icon-play, - &.loading .icon-pause { - opacity: 0; - transform: scale(0.5); - } - } - .download-button { position: absolute; width: 1.1875rem !important; @@ -227,6 +245,11 @@ align-items: flex-end; } + + &.non-interactive { + pointer-events: none; + } + .meta, .performer, .date { diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index 6b3e86954..5cc544b20 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -16,6 +16,7 @@ import { getMediaTransferState, getMessageMediaFormat, getMessageMediaHash, + hasMessageTtl, isMessageLocal, isOwnMessage, } from '../../global/helpers'; @@ -24,6 +25,7 @@ import buildClassName from '../../util/buildClassName'; import { captureEvents } from '../../util/captureEvents'; import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dateFormat'; import { decodeWaveform, interpolateArray } from '../../util/waveform'; +import { LOCAL_TGS_URLS } from './helpers/animatedAssets'; import { getFileSizeString } from './helpers/documentInfo'; import renderText from './helpers/renderText'; import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform'; @@ -40,6 +42,8 @@ import useShowTransition from '../../hooks/useShowTransition'; import Button from '../ui/Button'; import Link from '../ui/Link'; import ProgressSpinner from '../ui/ProgressSpinner'; +import AnimatedIcon from './AnimatedIcon'; +import Icon from './Icon'; import './Audio.scss'; @@ -61,8 +65,10 @@ type OwnProps = { canTranscribe?: boolean; isTranscriptionHidden?: boolean; isTranscriptionError?: boolean; + autoPlay?: boolean; onHideTranscription?: (isHidden: boolean) => 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 ( +