Introduce one-time voice message (#4182)

This commit is contained in:
Alexander Zinchuk 2024-02-06 16:48:24 +01:00
parent d43a525a43
commit 96d4e7a437
35 changed files with 555 additions and 130 deletions

View File

@ -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)

View File

@ -475,6 +475,8 @@ export type MediaContent = {
storyData?: ApiMessageStoryData;
giveaway?: ApiGiveaway;
giveawayResults?: ApiGiveawayResults;
ttlSeconds?: number;
isExpiredVoice?: boolean;
};
export interface ApiMessage {

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<g id="surface1">
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 16 29.328125 C 16.734375 29.328125 17.328125 28.734375 17.328125 28 C 17.328125 27.265625 16.734375 26.671875 16 26.671875 Z M 16 5.328125 C 16.734375 5.328125 17.328125 4.734375 17.328125 4 C 17.328125 3.265625 16.734375 2.671875 16 2.671875 Z M 16 26.671875 C 10.105469 26.671875 5.328125 21.894531 5.328125 16 L 2.671875 16 C 2.671875 23.359375 8.640625 29.328125 16 29.328125 Z M 5.328125 16 C 5.328125 10.105469 10.105469 5.328125 16 5.328125 L 16 2.671875 C 8.640625 2.671875 2.671875 8.640625 2.671875 16 Z M 5.328125 16 "/>
<path style=" stroke:none;fill-rule:evenodd;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 29.273438 14.777344 C 29.207031 14.046875 28.558594 13.507812 27.828125 13.574219 C 27.097656 13.640625 26.5625 14.289062 26.628906 15.019531 Z M 24.207031 9.179688 C 24.675781 9.742188 25.515625 9.820312 26.078125 9.351562 C 26.640625 8.882812 26.71875 8.042969 26.25 7.480469 Z M 24.519531 5.75 C 23.957031 5.28125 23.117188 5.359375 22.648438 5.921875 C 22.179688 6.484375 22.257812 7.324219 22.820312 7.792969 Z M 16.980469 5.371094 C 17.710938 5.4375 18.359375 4.902344 18.425781 4.171875 C 18.492188 3.441406 17.953125 2.792969 17.222656 2.726562 Z M 14.777344 2.726562 C 14.046875 2.792969 13.507812 3.441406 13.574219 4.171875 C 13.640625 4.902344 14.289062 5.4375 15.019531 5.371094 Z M 9.179688 7.792969 C 9.742188 7.324219 9.820312 6.484375 9.351562 5.921875 C 8.878906 5.359375 8.042969 5.28125 7.480469 5.75 Z M 5.75 7.480469 C 5.28125 8.042969 5.359375 8.882812 5.921875 9.351562 C 6.484375 9.820312 7.324219 9.742188 7.792969 9.179688 Z M 5.371094 15.019531 C 5.4375 14.289062 4.902344 13.640625 4.171875 13.574219 C 3.441406 13.507812 2.792969 14.046875 2.726562 14.777344 Z M 2.726562 17.222656 C 2.792969 17.953125 3.441406 18.492188 4.171875 18.425781 C 4.902344 18.359375 5.4375 17.710938 5.371094 16.980469 Z M 7.792969 22.820312 C 7.324219 22.257812 6.484375 22.179688 5.921875 22.648438 C 5.359375 23.121094 5.28125 23.957031 5.75 24.519531 Z M 7.480469 26.25 C 8.042969 26.71875 8.878906 26.640625 9.351562 26.078125 C 9.820312 25.515625 9.742188 24.675781 9.179688 24.207031 Z M 15.019531 26.628906 C 14.289062 26.5625 13.640625 27.097656 13.574219 27.828125 C 13.507812 28.558594 14.046875 29.207031 14.777344 29.273438 Z M 17.222656 29.273438 C 17.953125 29.207031 18.492188 28.558594 18.425781 27.828125 C 18.359375 27.097656 17.710938 26.5625 16.980469 26.628906 Z M 22.820312 24.207031 C 22.257812 24.675781 22.179688 25.515625 22.648438 26.078125 C 23.117188 26.640625 23.957031 26.71875 24.519531 26.25 Z M 26.25 24.519531 C 26.71875 23.957031 26.640625 23.121094 26.078125 22.648438 C 25.515625 22.179688 24.675781 22.257812 24.207031 22.820312 Z M 26.628906 16.980469 C 26.5625 17.710938 27.097656 18.359375 27.828125 18.425781 C 28.558594 18.492188 29.207031 17.953125 29.273438 17.222656 Z M 29.328125 16 C 29.328125 15.589844 29.308594 15.179688 29.273438 14.777344 L 26.628906 15.019531 C 26.65625 15.34375 26.671875 15.671875 26.671875 16 Z M 26.25 7.480469 C 25.726562 6.851562 25.148438 6.273438 24.519531 5.75 L 22.820312 7.792969 C 23.324219 8.210938 23.789062 8.675781 24.207031 9.179688 Z M 17.222656 2.726562 C 16.816406 2.691406 16.40625 2.671875 16 2.671875 L 16 5.328125 C 16.332031 5.328125 16.660156 5.34375 16.980469 5.371094 Z M 16 2.671875 C 15.589844 2.671875 15.179688 2.691406 14.777344 2.726562 L 15.019531 5.371094 C 15.34375 5.34375 15.671875 5.328125 16 5.328125 Z M 7.480469 5.75 C 6.851562 6.273438 6.273438 6.851562 5.75 7.480469 L 7.792969 9.179688 C 8.210938 8.675781 8.675781 8.210938 9.179688 7.792969 Z M 2.726562 14.777344 C 2.691406 15.183594 2.671875 15.59375 2.671875 16 L 5.328125 16 C 5.328125 15.667969 5.34375 15.339844 5.371094 15.019531 Z M 2.671875 16 C 2.671875 16.410156 2.691406 16.820312 2.726562 17.222656 L 5.371094 16.980469 C 5.34375 16.65625 5.328125 16.328125 5.328125 16 Z M 5.75 24.519531 C 6.273438 25.148438 6.851562 25.726562 7.480469 26.25 L 9.179688 24.207031 C 8.675781 23.789062 8.210938 23.324219 7.792969 22.820312 Z M 14.777344 29.273438 C 15.179688 29.308594 15.589844 29.328125 16 29.328125 L 16 26.671875 C 15.667969 26.671875 15.339844 26.65625 15.019531 26.628906 Z M 16 29.328125 C 16.410156 29.328125 16.820312 29.308594 17.222656 29.273438 L 16.980469 26.628906 C 16.65625 26.65625 16.328125 26.671875 16 26.671875 Z M 24.519531 26.25 C 25.148438 25.726562 25.726562 25.148438 26.25 24.519531 L 24.207031 22.820312 C 23.789062 23.324219 23.324219 23.789062 22.820312 24.207031 Z M 29.273438 17.222656 C 29.308594 16.820312 29.328125 16.410156 29.328125 16 L 26.671875 16 C 26.671875 16.332031 26.65625 16.660156 26.628906 16.980469 Z M 16.726562 22.996094 C 15.890625 22.996094 15.320312 22.433594 15.320312 21.582031 L 15.320312 11.898438 L 15.261719 11.898438 L 13.285156 13.273438 C 13.023438 13.460938 12.824219 13.527344 12.535156 13.527344 C 11.960938 13.527344 11.539062 13.117188 11.539062 12.515625 C 11.539062 12.085938 11.710938 11.765625 12.148438 11.457031 L 14.832031 9.601562 C 15.488281 9.148438 15.9375 9.066406 16.519531 9.066406 C 17.523438 9.066406 18.121094 9.675781 18.121094 10.648438 L 18.121094 21.582031 C 18.121094 22.433594 17.558594 22.996094 16.726562 22.996094 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

View File

@ -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';

View File

@ -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 {

View File

@ -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<OwnProps> = ({
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<boolean>(false);
// eslint-disable-next-line no-null/no-null
@ -109,10 +123,13 @@ const Audio: FC<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
});
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<OwnProps> = ({
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<OwnProps> = ({
);
}
function renderTooglePlayWrapper() {
return (
<div className="toogle-play-wrapper">
<Button
round
ripple={!isMobile}
size="smaller"
color={coverBlobUrl ? 'translucent-white' : 'primary'}
className={buttonClassNames.join(' ')}
ariaLabel={isPlaying ? 'Pause audio' : 'Play audio'}
onClick={handleButtonClick}
isRtl={lang.isRtl}
backgroundImage={coverBlobUrl}
nonInteractive={isOneTimeModalOrigin}
>
{!isOneTimeModalOrigin && <Icon name="play" />}
{!isOneTimeModalOrigin && <Icon name="pause" />}
{isOneTimeModalOrigin && (
<AnimatedIcon
className="flame"
tgsUrl={LOCAL_TGS_URLS.Flame}
nonInteractive
noLoop={false}
size={40}
/>
)}
</Button>
{hasTtl && !isOneTimeModalOrigin && (
<Icon name="view-once" />
)}
</div>
);
}
return (
<div className={fullClassName} dir={lang.isRtl ? 'rtl' : 'ltr'}>
{isSelectable && (
@ -338,31 +410,30 @@ const Audio: FC<OwnProps> = ({
{isSelected && <i className="icon icon-select" />}
</div>
)}
<Button
round
ripple={!isMobile}
size="smaller"
color={coverBlobUrl ? 'translucent-white' : 'primary'}
className={buttonClassNames.join(' ')}
ariaLabel={isPlaying ? 'Pause audio' : 'Play audio'}
onClick={handleButtonClick}
isRtl={lang.isRtl}
backgroundImage={coverBlobUrl}
>
<i className="icon icon-play" />
<i className="icon icon-pause" />
</Button>
{renderTooglePlayWrapper()}
{shouldRenderSpinner && (
<div className={buildClassName('media-loading', spinnerClassNames, shouldRenderCross && 'interactive')}>
<ProgressSpinner
progress={transferProgress}
transparent
withColor
size="m"
onClick={shouldRenderCross ? handleButtonClick : undefined}
noCross={!shouldRenderCross}
/>
</div>
)}
{isOneTimeModalOrigin && !shouldRenderSpinner && (
<div className={buildClassName('media-loading')}>
<ProgressSpinner
progress={playProgress}
transparent
size="m"
noCross
rotationOffset={3 / 4}
/>
</div>
)}
{audio && canDownload && !isUploading && (
<Button
round
@ -389,12 +460,12 @@ const Audio: FC<OwnProps> = ({
onDateClick ? handleDateClick : undefined,
)}
{origin === AudioOrigin.SharedMedia && (voice || video) && renderWithTitle()}
{origin === AudioOrigin.Inline && voice && (
{(origin === AudioOrigin.Inline || isOneTimeModalOrigin) && voice && (
renderVoice(
voice,
seekerRef,
waveformCanvasRef,
playProgress,
hasTtl ? reversePlayProgress : playProgress,
isMediaUnread,
isTranscribing,
isTranscriptionHidden,
@ -402,6 +473,7 @@ const Audio: FC<OwnProps> = ({
isTranscriptionError,
canTranscribe ? handleTranscribe : undefined,
onHideTranscription,
origin,
)
)}
</div>
@ -489,6 +561,7 @@ function renderVoice(
isTranscriptionError?: boolean,
onClickTranscribe?: VoidFunction,
onHideTranscription?: (isHidden: boolean) => void,
origin?: AudioOrigin,
) {
return (
<div className="content">
@ -537,8 +610,12 @@ function renderVoice(
</Button>
)}
</div>
<p className={buildClassName('voice-duration', isMediaUnread && 'unread')} dir="auto">
{playProgress === 0 ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}
<p
className={buildClassName('voice-duration', origin !== AudioOrigin.OneTimeModal && isMediaUnread && 'unread')}
dir="auto"
>
{playProgress === 0 || playProgress === 1
? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}
</p>
</div>
);
@ -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<HTMLCanvasElement>(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;
}

View File

@ -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,
};

View File

@ -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 [];
}

View File

@ -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 (
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
{getExpiredMessageDescription(lang, lastMessage)}
</p>
);
}
if (isAction) {
const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId);

View File

@ -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<OwnProps & StateProps> = ({
noRightColumnAnimation,
isSynced,
inviteViaLinkModal,
oneTimeMediaModal,
}) => {
const {
initMain,
@ -568,6 +571,7 @@ const Main: FC<OwnProps & StateProps> = ({
/>
<BoostModal info={boostModal} />
<GiftCodeModal modal={giftCodeModal} />
<OneTimeMediaModal info={oneTimeMediaModal} />
<ChatlistModal info={chatlistModal} />
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
<WebAppModal webApp={webApp} />
@ -635,6 +639,7 @@ export default memo(withGlobal<OwnProps>(
boostModal,
giftCodeModal,
inviteViaLinkModal,
oneTimeMediaModal,
} = selectTabState(global);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
@ -703,6 +708,7 @@ export default memo(withGlobal<OwnProps>(
noRightColumnAnimation,
isSynced: global.isSynced,
inviteViaLinkModal,
oneTimeMediaModal,
};
},
)(Main));

View File

@ -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,

View File

@ -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<OwnProps>(
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<OwnProps>(
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<OwnProps>(
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,

View File

@ -40,6 +40,7 @@ import {
getMessageSingleRegularEmoji,
getSenderTitle,
hasMessageText,
hasMessageTtl,
isAnonymousOwnMessage,
isChatChannel,
isChatGroup,
@ -524,6 +525,7 @@ const Message: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
&& !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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
isTranscriptionError={isTranscriptionError}
canDownload={!isProtected}
onHideTranscription={setTranscriptionHidden}
canTranscribe={isPremium}
canTranscribe={isPremium && !hasTtl}
/>
)}
{document && (

View File

@ -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<OwnProps> = (props) => {
const { info } = props;
const OneTimeMediaModal = useModuleLoader(Bundles.Extra, 'OneTimeMediaModal', !info);
// eslint-disable-next-line react/jsx-props-no-spreading
return OneTimeMediaModal ? <OneTimeMediaModal {...props} /> : undefined;
};
export default OneTimeMediaModalAsync;

View File

@ -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;
}
}

View File

@ -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 (
<Audio
theme={theme}
message={message}
origin={AudioOrigin.OneTimeModal}
autoPlay
onPlay={handlePlayVoice}
onPause={handleClose}
/>
);
}
return undefined;
}
return (
<div className={buildClassName(styles.root, transitionClassNames)}>
<div className={styles.main}>{renderMedia()}</div>
<div className={styles.footer}>
<Button
faded
onClick={handleClose}
pill
size="smaller"
color={theme === 'dark' ? 'dark' : 'secondary'}
className={styles.closeBtn}
>
{closeBtnTitle}
</Button>
</div>
</div>
);
};
export default memo(OneTimeMediaModal);

View File

@ -1,11 +1,13 @@
.ProgressSpinner {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
z-index: 1;
width: 3.375rem;
height: 3.375rem;
background: rgba(0, 0, 0, 0.25)
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMTkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEwLjcxNyA5Ljc1TDE4LjMgMi4xNjdhLjY4NC42ODQgMCAxMC0uOTY3LS45NjdMOS43NSA4Ljc4MyAyLjE2NyAxLjJhLjY4NC42ODQgMCAxMC0uOTY3Ljk2N0w4Ljc4MyA5Ljc1IDEuMiAxNy4zMzNhLjY4NC42ODQgMCAxMC45NjcuOTY3bDcuNTgzLTcuNTgzIDcuNTgzIDcuNTgzYS42ODEuNjgxIDAgMDAuOTY3IDAgLjY4NC42ODQgMCAwMDAtLjk2N0wxMC43MTcgOS43NXoiIGZpbGw9IiNGRkYiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjRkZGIiBzdHJva2Utd2lkdGg9Ii43NSIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==)
no-repeat 49% 49%;
background-color: rgba(0, 0, 0, 0.25);
border-radius: 50%;
cursor: var(--custom-cursor, pointer);
@ -14,6 +16,12 @@
pointer-events: none;
}
.icon-close {
position: absolute;
font-size: 1.625rem;
color: var(--color-white);
}
&.square {
background-image: none;
@ -34,10 +42,6 @@
&.size-m {
width: auto;
height: auto;
/* stylelint-disable-next-line scss/operator-no-unspaced */
background: transparent
url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUiIGhlaWdodD0iMTUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTguMjE4IDcuNWw1LjYzMy01LjYzM2EuNTA4LjUwOCAwIDEwLS43MTgtLjcxOEw3LjUgNi43ODIgMS44NjcgMS4xNDlhLjUwOC41MDggMCAxMC0uNzE4LjcxOEw2Ljc4MiA3LjVsLTUuNjMzIDUuNjMzYS41MDguNTA4IDAgMTAuNzE4LjcxOEw3LjUgOC4yMThsNS42MzMgNS42MzNhLjUwNi41MDYgMCAwMC43MTggMCAuNTA4LjUwOCAwIDAwMC0uNzE4TDguMjE4IDcuNXoiIGZpbGw9IiNGRkYiIGZpbGwtcnVsZT0ibm9uemVybyIgc3Ryb2tlPSIjRkZGIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+)
no-repeat 49% 49%;
&.square {
background-image: none;
@ -62,5 +66,6 @@
&_canvas {
display: block;
background-color: transparent !important;
color: var(--color-white);
}
}

View File

@ -5,9 +5,12 @@ import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { animate, timingFunctions } from '../../util/animation';
import buildClassName from '../../util/buildClassName';
import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListener';
import { useStateRef } from '../../hooks/useStateRef';
import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio';
import Icon from '../common/Icon';
import './ProgressSpinner.scss';
const SIZES = {
@ -27,6 +30,8 @@ const ProgressSpinner: FC<{
square?: boolean;
transparent?: boolean;
noCross?: boolean;
rotationOffset?: number;
withColor?: boolean;
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
}> = ({
progress = 0,
@ -34,6 +39,8 @@ const ProgressSpinner: FC<{
square,
transparent,
noCross,
rotationOffset,
withColor,
onClick,
}) => {
// eslint-disable-next-line no-null/no-null
@ -43,6 +50,8 @@ const ProgressSpinner: FC<{
const dpr = useDevicePixelRatio();
const color = useDynamicColorListener(canvasRef, !withColor);
useEffect(() => {
let isFirst = true;
let growFrom = MIN_PROGRESS;
@ -69,17 +78,18 @@ const ProgressSpinner: FC<{
canvasRef.current,
width * dpr,
(size === 'xl' ? STROKE_WIDTH_XL : STROKE_WIDTH) * dpr,
'white',
color ?? 'white',
currentProgress,
dpr,
isFirst,
rotationOffset,
);
isFirst = false;
return currentProgress < 1;
}, requestMutation);
}, [progressRef, size, width, dpr]);
}, [progressRef, size, width, dpr, rotationOffset, color]);
const className = buildClassName(
`ProgressSpinner size-${size}`,
@ -93,6 +103,7 @@ const ProgressSpinner: FC<{
className={className}
onClick={onClick}
>
{!noCross && <Icon name="close" />}
<canvas ref={canvasRef} className="ProgressSpinner_canvas" style={`width: ${width}; height: ${width}px;`} />
</div>
);
@ -106,11 +117,12 @@ function drawSpinnerArc(
progress: number,
dpr: number,
shouldInit = false,
rotationOffset?: number,
) {
const centerCoordinate = size / 2;
const radius = (size - strokeWidth) / 2 - PADDING * dpr;
const rotationOffset = (Date.now() % ROTATE_DURATION) / ROTATE_DURATION;
const startAngle = (2 * Math.PI) * rotationOffset;
const offset = rotationOffset ?? (Date.now() % ROTATE_DURATION) / ROTATE_DURATION;
const startAngle = (2 * Math.PI) * offset;
const endAngle = startAngle + (2 * Math.PI) * progress;
const ctx = canvas.getContext('2d')!;

View File

@ -52,7 +52,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false;
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v28';
export const LANG_CACHE_NAME = 'tt-lang-packs-v30';
export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';

View File

@ -445,6 +445,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const chatId = selectCommonBoxChatId(global, id);
if (chatId) {
global = updateChatMessage(global, chatId, id, messageUpdate);
const message = selectChatMessage(global, chatId, id);
if (message) {
global = updateChatLastMessage(global, chatId, message);
}
}
});

View File

@ -813,6 +813,24 @@ addActionHandler('copyMessagesByIds', (global, actions, payload): ActionReturnTy
copyTextForMessages(global, chat.id, messageIds);
});
addActionHandler('openOneTimeMediaModal', (global, actions, payload): ActionReturnType => {
const { message, tabId = getCurrentTabId() } = payload;
global = updateTabState(global, {
oneTimeMediaModal: {
message,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeOneTimeMediaModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
global = updateTabState(global, {
oneTimeMediaModal: undefined,
}, tabId);
setGlobal(global);
});
function copyTextForMessages(global: GlobalState, chatId: string, messageIds: number[]) {
const { type: messageListType, threadId } = selectCurrentMessageList(global) || {};
const lang = langProvider.translate;

View File

@ -7,7 +7,9 @@ import { ApiMessageEntityTypes } from '../../api/types';
import { CONTENT_NOT_SUPPORTED } from '../../config';
import trimText from '../../util/trimText';
import { getGlobal } from '../index';
import { getMessageText, getMessageTranscription } from './messages';
import {
getExpiredMessageDescription, getMessageText, getMessageTranscription, isExpiredMessage,
} from './messages';
import { getUserFirstOrLastName } from './users';
const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
@ -213,6 +215,13 @@ export function getMessageSummaryDescription(
}
}
if (isExpiredMessage(message)) {
const expiredMessageText = getExpiredMessageDescription(lang, message);
if (expiredMessageText) {
summary = expiredMessageText;
}
}
return summary || CONTENT_NOT_SUPPORTED;
}

View File

@ -55,12 +55,12 @@ export function getMessageTranscription(message: ApiMessage) {
export function hasMessageText(message: ApiMessage | ApiStory) {
const {
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
game, action, storyData, giveaway, giveawayResults,
game, action, storyData, giveaway, giveawayResults, isExpiredVoice,
} = message.content;
return Boolean(text) || !(
sticker || photo || video || audio || voice || document || contact || poll || webPage || invoice || location
|| game || action?.phoneCall || storyData || giveaway || giveawayResults
|| game || action?.phoneCall || storyData || giveaway || giveawayResults || isExpiredVoice
);
}
@ -173,7 +173,7 @@ export function isForwardedMessage(message: ApiMessage) {
}
export function isActionMessage(message: ApiMessage) {
return Boolean(message.content.action);
return Boolean(message.content.action) || isExpiredMessage(message);
}
export function isServiceNotificationMessage(message: ApiMessage) {
@ -338,3 +338,19 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList =
return { text, entities };
}
export function getExpiredMessageDescription(langFn: LangFn, message: ApiMessage): string | undefined {
const { isExpiredVoice } = message.content;
if (isExpiredVoice) {
return langFn('Message.VoiceMessageExpired');
}
return undefined;
}
export function isExpiredMessage(message: ApiMessage) {
return Boolean(message.content?.isExpiredVoice);
}
export function hasMessageTtl(message: ApiMessage) {
return message.content?.ttlSeconds !== undefined;
}

View File

@ -17,6 +17,7 @@ import {
areSortedArraysEqual, excludeSortedArray, omit, pick, pickTruthy, unique,
} from '../../util/iteratees';
import {
hasMessageTtl,
isLocalMessageId, mergeIdRanges, orderHistoryIds, orderPinnedIds,
} from '../helpers';
import {
@ -198,6 +199,15 @@ export function updateChatMessage<T extends GlobalState>(
): T {
const byId = selectChatMessages(global, chatId) || {};
const message = byId[messageId];
if (message && messageUpdate.isMediaUnread === false && hasMessageTtl(message)) {
if (message.content.voice) {
messageUpdate.content = {
...messageUpdate.content,
voice: undefined,
isExpiredVoice: true,
};
}
}
const updatedMessage = {
...message,
...messageUpdate,

View File

@ -37,6 +37,7 @@ import {
getMessageWebPagePhoto,
getMessageWebPageVideo,
getSendingState,
hasMessageTtl,
isActionMessage,
isChatBasicGroup,
isChatChannel,
@ -534,6 +535,7 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
const isOwn = isOwnMessage(message);
const isForwarded = isForwardedMessage(message);
const isAction = isActionMessage(message);
const hasTtl = hasMessageTtl(message);
const { content } = message;
const messageTopic = selectTopicFromMessage(global, message);
@ -607,7 +609,7 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
const isStoryForwardForbidden = story && ('isDeleted' in story || ('noForwards' in story && story.noForwards));
const canForward = (
!isLocal && !isAction && !isChatProtected && !isStoryForwardForbidden
&& (message.isForwardingAllowed || isServiceNotification)
&& (message.isForwardingAllowed || isServiceNotification) && !hasTtl
);
const hasSticker = Boolean(message.content.sticker);
@ -619,7 +621,8 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
const canSelect = !isLocal && !isAction;
const canDownload = Boolean(content.webPage?.document || content.webPage?.video || content.webPage?.photo
|| content.audio || content.voice || content.photo || content.video || content.document || content.sticker);
|| content.audio || content.voice || content.photo || content.video || content.document || content.sticker)
&& !hasTtl;
const canSaveGif = message.content.video?.isGif;
@ -1113,7 +1116,9 @@ export function selectLastServiceNotification<T extends GlobalState>(global: T)
}
export function selectIsMessageProtected<T extends GlobalState>(global: T, message?: ApiMessage) {
return Boolean(message && (message.isProtected || selectIsChatProtected(global, message.chatId)));
return Boolean(message && (
message.isProtected || selectIsChatProtected(global, message.chatId) || hasMessageTtl(message)
));
}
export function selectIsChatProtected<T extends GlobalState>(global: T, chatId: string) {
@ -1147,7 +1152,8 @@ export function selectCanForwardMessages<T extends GlobalState>(global: T, chatI
return messageIds
.map((id) => messages[id])
.every((message) => message.isForwardingAllowed || isServiceNotificationMessage(message));
.every((message) => !hasMessageTtl(message)
&& (message.isForwardingAllowed || isServiceNotificationMessage(message)));
}
export function selectSponsoredMessage<T extends GlobalState>(global: T, chatId: string) {

View File

@ -674,6 +674,10 @@ export type TabState = {
restrictedUserIds: string[];
chatId: string;
};
oneTimeMediaModal?: {
message: ApiMessage;
};
};
export type GlobalState = {
@ -2679,6 +2683,9 @@ export interface ActionPayloads {
updatePageTitle: WithTabId | undefined;
closeInviteViaLinkModal: WithTabId | undefined;
openOneTimeMediaModal: TabState['oneTimeMediaModal'] & WithTabId;
closeOneTimeMediaModal: WithTabId | undefined;
// Calls
joinGroupCall: {
chatId?: string;

View File

@ -32,6 +32,8 @@ const useAudioPlayer = (
onTrackChange?: NoneToVoidFunction,
noPlaylist = false,
noProgressUpdates = false,
onPause?: NoneToVoidFunction,
noReset = false,
) => {
// eslint-disable-next-line no-null/no-null
const controllerRef = useRef<ReturnType<typeof register>>(null);
@ -54,8 +56,9 @@ const useAudioPlayer = (
setVolume, setPlaybackRate, toggleMuted, proxy,
} = controllerRef.current!;
setIsPlaying(true);
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
if (trackType !== 'oneTimeVoice') {
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
}
setPlaybackState('playing');
const { audioPlayer } = selectTabState(getGlobal());
setVolume(audioPlayer.volume);
@ -84,9 +87,13 @@ const useAudioPlayer = (
case 'onPause':
setIsPlaying(false);
setPlaybackState('paused');
onPause?.();
break;
case 'onTimeUpdate': {
const { proxy } = controllerRef.current!;
if (noReset && proxy.currentTime === 0) {
break;
}
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
if (!noProgressUpdates) setPlayProgress(proxy.currentTime / duration);
break;
@ -137,10 +144,13 @@ const useAudioPlayer = (
// RAF progress
useEffect(() => {
if (noReset && proxy.currentTime === 0) {
return;
}
if (duration && !isSafariPatchInProgress(proxy) && !noProgressUpdates) {
setPlayProgress(proxy.currentTime / duration);
}
}, [duration, playProgress, proxy, noProgressUpdates]);
}, [duration, playProgress, proxy, noProgressUpdates, noReset]);
// Cleanup
useEffect(() => () => {
@ -149,7 +159,7 @@ const useAudioPlayer = (
// Autoplay once `src` is present
useEffectWithPrevDeps(([prevShouldPlay, prevSrc]) => {
if (prevShouldPlay === shouldPlay && src === prevSrc) {
if (prevShouldPlay === shouldPlay && src === prevSrc && trackType !== 'oneTimeVoice') {
return;
}
@ -161,7 +171,7 @@ const useAudioPlayer = (
if (shouldPlay && src && !isPlaying) {
play(src);
}
}, [shouldPlay, src, isPlaying, play, proxy.src, proxy.paused]);
}, [shouldPlay, src, isPlaying, play, proxy.src, proxy.paused, trackType]);
const playIfPresent = useLastCallback(() => {
if (src) {

View File

@ -241,15 +241,16 @@ $icons-map: (
"video-outlined": "\f1d2",
"video-stop": "\f1d3",
"video": "\f1d4",
"voice-chat": "\f1d5",
"volume-1": "\f1d6",
"volume-2": "\f1d7",
"volume-3": "\f1d8",
"web": "\f1d9",
"webapp": "\f1da",
"word-wrap": "\f1db",
"zoom-in": "\f1dc",
"zoom-out": "\f1dd",
"view-once": "\f1d5",
"voice-chat": "\f1d6",
"volume-1": "\f1d7",
"volume-2": "\f1d8",
"volume-3": "\f1d9",
"web": "\f1da",
"webapp": "\f1db",
"word-wrap": "\f1dc",
"zoom-in": "\f1dd",
"zoom-out": "\f1de",
);
.icon-active-sessions::before {
@ -888,6 +889,9 @@ $icons-map: (
.icon-video::before {
content: map.get($icons-map, "video");
}
.icon-view-once::before {
content: map.get($icons-map, "view-once");
}
.icon-voice-chat::before {
content: map.get($icons-map, "voice-chat");
}

Binary file not shown.

Binary file not shown.

View File

@ -211,6 +211,7 @@ export type FontIconName =
| 'video-outlined'
| 'video-stop'
| 'video'
| 'view-once'
| 'voice-chat'
| 'volume-1'
| 'volume-2'

View File

@ -310,6 +310,7 @@ export enum AudioOrigin {
Inline,
SharedMedia,
Search,
OneTimeModal,
}
export enum ChatCreationProgress {

View File

@ -17,7 +17,7 @@ export type TrackId = `${MessageKey}-${number}`;
export interface Track {
audio: HTMLAudioElement;
proxy: HTMLAudioElement;
type: 'voice' | 'audio';
type: 'voice' | 'audio' | 'oneTimeVoice';
handlers: Handler[];
onForcePlay?: NoneToVoidFunction;
onTrackChange?: NoneToVoidFunction;

View File

@ -1,5 +1,6 @@
import { DEBUG, DEBUG_ALERT_MSG } from '../config';
import { isMasterTab } from './establishMultitabRole';
import { throttle } from './schedulers';
let showError = true;
let error: Error | undefined;
@ -27,16 +28,20 @@ if (DEBUG) {
});
}
const throttleError = throttle((err) => {
if (showError) {
// eslint-disable-next-line no-alert
window.alert(getErrorMessage(err));
} else {
error = err;
}
}, 1500);
export function handleError(err: Error) {
// eslint-disable-next-line no-console
console.error(err);
if (DEBUG) {
if (showError) {
// eslint-disable-next-line no-alert
window.alert(getErrorMessage(err));
} else {
error = err;
}
throttleError(err);
}
}