= ({
isTranscriptionError,
canTranscribe ? handleTranscribe : undefined,
onHideTranscription,
+ origin,
)
)}
@@ -489,6 +561,7 @@ function renderVoice(
isTranscriptionError?: boolean,
onClickTranscribe?: VoidFunction,
onHideTranscription?: (isHidden: boolean) => void,
+ origin?: AudioOrigin,
) {
return (
@@ -537,8 +610,12 @@ function renderVoice(
)}
-
- {playProgress === 0 ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}
+
+ {playProgress === 0 || playProgress === 1
+ ? formatMediaDuration(voice.duration) : formatMediaDuration(voice.duration * playProgress)}
);
@@ -551,6 +628,7 @@ function useWaveformCanvas(
isOwn = false,
withAvatar = false,
isMobile = false,
+ isReverse = false,
) {
// eslint-disable-next-line no-null/no-null
const canvasRef = useRef(null);
@@ -588,12 +666,15 @@ function useWaveformCanvas(
const progressFillColor = theme === 'dark' ? '#8774E1' : '#3390EC';
const progressFillOwnColor = theme === 'dark' ? '#FFFFFF' : '#4FAE4E';
- renderWaveform(canvas, spikes, playProgress, {
+ const fillStyle = isOwn ? fillOwnColor : fillColor;
+ const progressFillStyle = isOwn ? progressFillOwnColor : progressFillColor;
+
+ renderWaveform(canvas, spikes, isReverse ? 1 - playProgress : playProgress, {
peak,
- fillStyle: isOwn ? fillOwnColor : fillColor,
- progressFillStyle: isOwn ? progressFillOwnColor : progressFillColor,
+ fillStyle,
+ progressFillStyle,
});
- }, [isOwn, peak, playProgress, spikes, theme]);
+ }, [isOwn, peak, playProgress, spikes, theme, isReverse]);
return canvasRef;
}
diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts
index a1fac8420..54350bfba 100644
--- a/src/components/common/helpers/animatedAssets.ts
+++ b/src/components/common/helpers/animatedAssets.ts
@@ -7,6 +7,7 @@ import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs';
import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs';
import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs';
import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs';
+import Flame from '../../../assets/tgs/general/Flame.tgs';
import PartyPopper from '../../../assets/tgs/general/PartyPopper.tgs';
import Invite from '../../../assets/tgs/invites/Invite.tgs';
import JoinRequest from '../../../assets/tgs/invites/Requests.tgs';
@@ -46,4 +47,5 @@ export const LOCAL_TGS_URLS = {
Congratulations,
Experimental,
PartyPopper,
+ Flame,
};
diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx
index d34ffb56b..0ea61633c 100644
--- a/src/components/common/helpers/renderActionMessageText.tsx
+++ b/src/components/common/helpers/renderActionMessageText.tsx
@@ -9,8 +9,10 @@ import type { TextPart } from '../../../types';
import {
getChatTitle,
+ getExpiredMessageDescription,
getMessageSummaryText,
getUserFullName,
+ isExpiredMessage,
} from '../../../global/helpers';
import { formatCurrency } from '../../../util/formatCurrency';
import trimText from '../../../util/trimText';
@@ -45,6 +47,10 @@ export function renderActionMessageText(
observeIntersectionForLoading?: ObserveFn,
observeIntersectionForPlaying?: ObserveFn,
) {
+ if (isExpiredMessage(message)) {
+ return getExpiredMessageDescription(lang, message);
+ }
+
if (!message.content.action) {
return [];
}
diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx
index 5caf68235..03e9be663 100644
--- a/src/components/left/main/hooks/useChatListEntry.tsx
+++ b/src/components/left/main/hooks/useChatListEntry.tsx
@@ -13,6 +13,7 @@ import type { LangFn } from '../../../../hooks/useLang';
import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
import {
+ getExpiredMessageDescription,
getMessageIsSpoiler,
getMessageMediaHash,
getMessageMediaThumbDataUri,
@@ -22,6 +23,7 @@ import {
getMessageVideo,
isActionMessage,
isChatChannel,
+ isExpiredMessage,
} from '../../../../global/helpers';
import { getMessageReplyInfo } from '../../../../global/helpers/replies';
import buildClassName from '../../../../util/buildClassName';
@@ -127,6 +129,14 @@ export default function useChatListEntry({
return undefined;
}
+ if (isExpiredMessage(lastMessage)) {
+ return (
+
+ {getExpiredMessageDescription(lang, lastMessage)}
+
+ );
+ }
+
if (isAction) {
const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId);
diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx
index 5fd7a821f..9fb1ab657 100644
--- a/src/components/main/Main.tsx
+++ b/src/components/main/Main.tsx
@@ -80,6 +80,7 @@ import BoostModal from '../modals/boost/BoostModal.async';
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
import GiftCodeModal from '../modals/giftcode/GiftCodeModal.async';
import MapModal from '../modals/map/MapModal.async';
+import OneTimeMediaModal from '../modals/oneTimeMedia/OneTimeMediaModal.async';
import UrlAuthModal from '../modals/urlAuth/UrlAuthModal.async';
import WebAppModal from '../modals/webApp/WebAppModal.async';
import PaymentModal from '../payment/PaymentModal.async';
@@ -163,6 +164,7 @@ type StateProps = {
withInterfaceAnimations?: boolean;
isSynced?: boolean;
inviteViaLinkModal?: TabState['inviteViaLinkModal'];
+ oneTimeMediaModal?: TabState['oneTimeMediaModal'];
};
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
@@ -224,6 +226,7 @@ const Main: FC = ({
noRightColumnAnimation,
isSynced,
inviteViaLinkModal,
+ oneTimeMediaModal,
}) => {
const {
initMain,
@@ -568,6 +571,7 @@ const Main: FC = ({
/>
+
@@ -635,6 +639,7 @@ export default memo(withGlobal(
boostModal,
giftCodeModal,
inviteViaLinkModal,
+ oneTimeMediaModal,
} = selectTabState(global);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
@@ -703,6 +708,7 @@ export default memo(withGlobal(
noRightColumnAnimation,
isSynced: global.isSynced,
inviteViaLinkModal,
+ oneTimeMediaModal,
};
},
)(Main));
diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx
index 045e50524..3428951fc 100644
--- a/src/components/middle/ActionMessage.tsx
+++ b/src/components/middle/ActionMessage.tsx
@@ -12,7 +12,9 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { FocusDirection } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
-import { getChatTitle, getMessageHtmlId, isChatChannel } from '../../global/helpers';
+import {
+ getChatTitle, getMessageHtmlId, isChatChannel,
+} from '../../global/helpers';
import { getMessageReplyInfo } from '../../global/helpers/replies';
import {
selectCanPlayAnimatedEmojis,
diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx
index 62d529f8f..8f09e5fac 100644
--- a/src/components/middle/message/ContextMenuContainer.tsx
+++ b/src/components/middle/message/ContextMenuContainer.tsx
@@ -14,6 +14,7 @@ import { PREVIEW_AVATAR_COUNT, SERVICE_NOTIFICATIONS_USER_ID } from '../../../co
import {
areReactionsEmpty,
getMessageVideo,
+ hasMessageTtl,
isActionMessage,
isChatChannel,
isChatGroup,
@@ -649,6 +650,7 @@ export default memo(withGlobal(
const isScheduled = messageListType === 'scheduled';
const isChannel = chat && isChatChannel(chat);
const isLocal = isMessageLocal(message);
+ const hasTtl = hasMessageTtl(message);
const canShowSeenBy = Boolean(!isLocal
&& chat
&& seenByMaxChatMembers
@@ -695,7 +697,7 @@ export default memo(withGlobal(
canForward: !isScheduled && canForward,
canFaveSticker: !isScheduled && canFaveSticker,
canUnfaveSticker: !isScheduled && canUnfaveSticker,
- canCopy: canCopyNumber || (!isProtected && canCopy),
+ canCopy: (canCopyNumber || (!isProtected && canCopy)),
canCopyLink: !isScheduled && canCopyLink,
canSelect,
canDownload: !isProtected && canDownload,
@@ -710,7 +712,8 @@ export default memo(withGlobal(
isCurrentUserPremium,
hasFullInfo: Boolean(chatFullInfo),
canShowReactionsCount,
- canShowReactionList: !isLocal && !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID,
+ canShowReactionList: !isLocal && !isAction
+ && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID && !hasTtl,
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
customEmojiSetsInfo,
customEmojiSets,
diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx
index de860d127..499dccebb 100644
--- a/src/components/middle/message/Message.tsx
+++ b/src/components/middle/message/Message.tsx
@@ -40,6 +40,7 @@ import {
getMessageSingleRegularEmoji,
getSenderTitle,
hasMessageText,
+ hasMessageTtl,
isAnonymousOwnMessage,
isChatChannel,
isChatGroup,
@@ -524,6 +525,7 @@ const Message: FC = ({
const messageColorPeer = originSender || sender;
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
const hasText = hasMessageText(message);
+ const hasTtl = hasMessageTtl(message);
const {
handleMouseDown,
@@ -671,7 +673,7 @@ const Message: FC = ({
&& !isInDocumentGroupNotLast && messageListType === 'thread'
&& !noComments;
const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction
- && !isInDocumentGroupNotLast && !isStoryMention;
+ && !isInDocumentGroupNotLast && !isStoryMention && !hasTtl;
const contentClassName = buildContentClassName(message, {
hasSubheader,
@@ -1107,7 +1109,7 @@ const Message: FC = ({
isSelected={isSelected}
noAvatars={noAvatars}
onPlay={handleAudioPlay}
- onReadMedia={voice && (!isOwn || isChatWithSelf) ? handleReadMedia : undefined}
+ onReadMedia={voice && (!isOwn || isChatWithSelf || (isOwn && !hasTtl)) ? handleReadMedia : undefined}
onCancelUpload={handleCancelUpload}
isDownloading={isDownloading}
isTranscribing={isTranscribing}
@@ -1116,7 +1118,7 @@ const Message: FC = ({
isTranscriptionError={isTranscriptionError}
canDownload={!isProtected}
onHideTranscription={setTranscriptionHidden}
- canTranscribe={isPremium}
+ canTranscribe={isPremium && !hasTtl}
/>
)}
{document && (
diff --git a/src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx b/src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx
new file mode 100644
index 000000000..6edd5b050
--- /dev/null
+++ b/src/components/modals/oneTimeMedia/OneTimeMediaModal.async.tsx
@@ -0,0 +1,18 @@
+import type { FC } from '../../../lib/teact/teact';
+import React from '../../../lib/teact/teact';
+
+import type { OwnProps } from './OneTimeMediaModal';
+
+import { Bundles } from '../../../util/moduleLoader';
+
+import useModuleLoader from '../../../hooks/useModuleLoader';
+
+const OneTimeMediaModalAsync: FC = (props) => {
+ const { info } = props;
+ const OneTimeMediaModal = useModuleLoader(Bundles.Extra, 'OneTimeMediaModal', !info);
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return OneTimeMediaModal ? : undefined;
+};
+
+export default OneTimeMediaModalAsync;
diff --git a/src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss b/src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss
new file mode 100644
index 000000000..002e2863f
--- /dev/null
+++ b/src/components/modals/oneTimeMedia/OneTimeMediaModal.module.scss
@@ -0,0 +1,49 @@
+
+.root {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ backdrop-filter: blur(2rem);
+ animation: fade-in-opacity 0.3s ease;
+ background-color: rgba(0, 0, 0, 0.25);
+ z-index: var(--z-modal-confirm);
+ align-items: center;
+ transition: opacity 0.3s ease;
+
+ &.closing {
+ opacity: 0;
+ }
+}
+
+.main {
+ background-color: var(--color-background);
+ padding: 0.6875rem;
+ border-radius: 1rem;
+}
+
+.footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ margin-bottom: 2rem;
+}
+
+.closeBtn {
+ margin: 0 auto;
+ width: auto;
+}
+
+@keyframes fade-in-opacity {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx b/src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx
new file mode 100644
index 000000000..20fd968af
--- /dev/null
+++ b/src/components/modals/oneTimeMedia/OneTimeMediaModal.tsx
@@ -0,0 +1,91 @@
+import React, { memo } from '../../../lib/teact/teact';
+import { getActions, getGlobal } from '../../../global';
+
+import type { TabState } from '../../../global/types';
+import { AudioOrigin } from '../../../types';
+
+import { isOwnMessage } from '../../../global/helpers';
+import { selectTheme } from '../../../global/selectors';
+import buildClassName from '../../../util/buildClassName';
+
+import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
+import useLang from '../../../hooks/useLang';
+import useLastCallback from '../../../hooks/useLastCallback';
+import useShowTransition from '../../../hooks/useShowTransition';
+
+import Audio from '../../common/Audio';
+import Button from '../../ui/Button';
+
+import styles from './OneTimeMediaModal.module.scss';
+
+export type OwnProps = {
+ info: TabState['oneTimeMediaModal'];
+};
+
+const OneTimeMediaModal = ({
+ info,
+}: OwnProps) => {
+ const {
+ closeOneTimeMediaModal,
+ } = getActions();
+
+ const lang = useLang();
+ const message = useCurrentOrPrev(info?.message, true);
+
+ const {
+ shouldRender,
+ transitionClassNames,
+ } = useShowTransition(Boolean(info));
+
+ const handlePlayVoice = useLastCallback(() => {
+ return undefined;
+ });
+
+ const handleClose = useLastCallback(() => {
+ closeOneTimeMediaModal();
+ });
+
+ if (!shouldRender || !message) {
+ return undefined;
+ }
+
+ const isOwn = isOwnMessage(message);
+ const theme = selectTheme(getGlobal());
+ const closeBtnTitle = isOwn ? lang('Chat.Voice.Single.Close') : lang('Chat.Voice.Single.DeleteAndClose');
+
+ function renderMedia() {
+ if (message?.content?.voice) {
+ return (
+
+ );
+ }
+ return undefined;
+ }
+
+ return (
+
+
{renderMedia()}
+
+
+
+
+ );
+};
+
+export default memo(OneTimeMediaModal);
diff --git a/src/components/ui/ProgressSpinner.scss b/src/components/ui/ProgressSpinner.scss
index cd3157ff3..6a9f4fe68 100644
--- a/src/components/ui/ProgressSpinner.scss
+++ b/src/components/ui/ProgressSpinner.scss
@@ -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);
}
}
diff --git a/src/components/ui/ProgressSpinner.tsx b/src/components/ui/ProgressSpinner.tsx
index 764c3fd1c..e05bdb267 100644
--- a/src/components/ui/ProgressSpinner.tsx
+++ b/src/components/ui/ProgressSpinner.tsx
@@ -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) => 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 && }
);
@@ -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')!;
diff --git a/src/config.ts b/src/config.ts
index 262ab9a85..4e36bf170 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -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';
diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts
index 9c85102b6..39ab15ecd 100644
--- a/src/global/actions/apiUpdaters/messages.ts
+++ b/src/global/actions/apiUpdaters/messages.ts
@@ -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);
+ }
}
});
diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts
index dd943b413..07c2d5ade 100644
--- a/src/global/actions/ui/messages.ts
+++ b/src/global/actions/ui/messages.ts
@@ -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;
diff --git a/src/global/helpers/messageSummary.ts b/src/global/helpers/messageSummary.ts
index 36e1e76c5..9b4c154c5 100644
--- a/src/global/helpers/messageSummary.ts
+++ b/src/global/helpers/messageSummary.ts
@@ -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;
}
diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts
index f9c86f409..e4bec9ae4 100644
--- a/src/global/helpers/messages.ts
+++ b/src/global/helpers/messages.ts
@@ -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;
+}
diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts
index 14f254ccd..321a6be54 100644
--- a/src/global/reducers/messages.ts
+++ b/src/global/reducers/messages.ts
@@ -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 {
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,
diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts
index 9841f2315..8f260141f 100644
--- a/src/global/selectors/messages.ts
+++ b/src/global/selectors/messages.ts
@@ -37,6 +37,7 @@ import {
getMessageWebPagePhoto,
getMessageWebPageVideo,
getSendingState,
+ hasMessageTtl,
isActionMessage,
isChatBasicGroup,
isChatChannel,
@@ -534,6 +535,7 @@ export function selectAllowedMessageActions(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(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(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(global: T)
}
export function selectIsMessageProtected(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(global: T, chatId: string) {
@@ -1147,7 +1152,8 @@ export function selectCanForwardMessages(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(global: T, chatId: string) {
diff --git a/src/global/types.ts b/src/global/types.ts
index 81f0677d6..16e057e1d 100644
--- a/src/global/types.ts
+++ b/src/global/types.ts
@@ -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;
diff --git a/src/hooks/useAudioPlayer.ts b/src/hooks/useAudioPlayer.ts
index 8350ba9f4..21cb043c9 100644
--- a/src/hooks/useAudioPlayer.ts
+++ b/src/hooks/useAudioPlayer.ts
@@ -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>(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) {
diff --git a/src/styles/icons.scss b/src/styles/icons.scss
index 4116eb0c1..1c1b4d47e 100644
--- a/src/styles/icons.scss
+++ b/src/styles/icons.scss
@@ -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");
}
diff --git a/src/styles/icons.woff b/src/styles/icons.woff
index 1c5163d5d..f12270ce9 100644
Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ
diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2
index 02af97acd..bde5a87f1 100644
Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ
diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts
index 26f097279..7d153cceb 100644
--- a/src/types/icons/font.ts
+++ b/src/types/icons/font.ts
@@ -211,6 +211,7 @@ export type FontIconName =
| 'video-outlined'
| 'video-stop'
| 'video'
+ | 'view-once'
| 'voice-chat'
| 'volume-1'
| 'volume-2'
diff --git a/src/types/index.ts b/src/types/index.ts
index 99143d0ac..d941de162 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -310,6 +310,7 @@ export enum AudioOrigin {
Inline,
SharedMedia,
Search,
+ OneTimeModal,
}
export enum ChatCreationProgress {
diff --git a/src/util/audioPlayer.ts b/src/util/audioPlayer.ts
index 13ff6421d..92e124769 100644
--- a/src/util/audioPlayer.ts
+++ b/src/util/audioPlayer.ts
@@ -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;
diff --git a/src/util/handleError.ts b/src/util/handleError.ts
index 2df36ba6e..cc00c1f64 100644
--- a/src/util/handleError.ts
+++ b/src/util/handleError.ts
@@ -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);
}
}