Introduce one-time voice message (#4182)
This commit is contained in:
parent
d43a525a43
commit
96d4e7a437
@ -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)
|
||||
|
||||
@ -475,6 +475,8 @@ export type MediaContent = {
|
||||
storyData?: ApiMessageStoryData;
|
||||
giveaway?: ApiGiveaway;
|
||||
giveawayResults?: ApiGiveawayResults;
|
||||
ttlSeconds?: number;
|
||||
isExpiredVoice?: boolean;
|
||||
};
|
||||
|
||||
export interface ApiMessage {
|
||||
|
||||
6
src/assets/font-icons/view-once.svg
Normal file
6
src/assets/font-icons/view-once.svg
Normal 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 |
BIN
src/assets/tgs/general/Flame.tgs
Normal file
BIN
src/assets/tgs/general/Flame.tgs
Normal file
Binary file not shown.
@ -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';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
91
src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx
Normal file
91
src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx
Normal 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);
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')!;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
@ -211,6 +211,7 @@ export type FontIconName =
|
||||
| 'video-outlined'
|
||||
| 'video-stop'
|
||||
| 'video'
|
||||
| 'view-once'
|
||||
| 'voice-chat'
|
||||
| 'volume-1'
|
||||
| 'volume-2'
|
||||
|
||||
@ -310,6 +310,7 @@ export enum AudioOrigin {
|
||||
Inline,
|
||||
SharedMedia,
|
||||
Search,
|
||||
OneTimeModal,
|
||||
}
|
||||
|
||||
export enum ChatCreationProgress {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user