Message: Introduce Invoice Media (#2093)

This commit is contained in:
Alexander Zinchuk 2022-11-07 23:00:55 +04:00
parent 58413c6c61
commit 6e8b920525
29 changed files with 528 additions and 73 deletions

View File

@ -1,7 +1,4 @@
import { Api as GramJs } from '../../../lib/gramjs';
import {
ApiMessageEntityTypes,
} from '../../types';
import type {
ApiMessage,
ApiMessageForwardInfo,
@ -36,6 +33,10 @@ import type {
PhoneCallAction,
ApiWebDocument,
ApiMessageEntityDefault,
ApiMessageExtendedMediaPreview,
} from '../../types';
import {
ApiMessageEntityTypes,
} from '../../types';
import {
@ -161,16 +162,21 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
content.action = action;
}
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
&& Boolean(mtpMessage.media.extendedMedia);
const { replyToMsgId, replyToTopId, replyToPeerId } = mtpMessage.replyTo || {};
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
const {
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse,
} = buildReplyButtons(mtpMessage) || {};
} = buildReplyButtons(mtpMessage, isInvoiceMedia) || {};
const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf);
const { replies, mediaUnread: isMediaUnread, postAuthor } = mtpMessage;
const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId);
const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker);
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
const isProtected = mtpMessage.noforwards || isInvoiceMedia;
const isForwardingAllowed = !mtpMessage.noforwards;
const emojiOnlyCount = content.text && parseEmojiOnlyString(content.text.text);
return {
@ -205,7 +211,8 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
...(replies?.comments && { threadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
...(postAuthor && { adminTitle: postAuthor }),
...(mtpMessage.noforwards && { isProtected: true }),
isProtected,
isForwardingAllowed,
};
}
@ -336,6 +343,10 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMes
return undefined;
}
if ('extendedMedia' in media && media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
return buildMessageMediaContent(media.extendedMedia.media);
}
const sticker = buildSticker(media);
if (sticker) return { sticker };
@ -748,9 +759,12 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A
export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
const {
description: text, title, photo, test, totalAmount, currency, receiptMsgId,
description: text, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia,
} = media;
const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview
? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined;
return {
title,
text,
@ -759,6 +773,7 @@ export function buildInvoice(media: GramJs.MessageMediaInvoice): ApiInvoice {
amount: Number(totalAmount),
currency,
isTest: test,
extendedMedia: preview,
};
}
@ -1007,7 +1022,7 @@ function buildAction(
};
}
function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefined {
function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: boolean): ApiReplyKeyboard | undefined {
const { replyMarkup, media } = message;
// TODO Move to the proper button inside preview
@ -1033,7 +1048,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
}
const markup = replyMarkup.rows.map(({ buttons }) => {
return buttons.map((button): ApiKeyboardButton => {
return buttons.map((button): ApiKeyboardButton | undefined => {
const { text } = button;
if (button instanceof GramJs.KeyboardButton) {
@ -1096,6 +1111,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
receiptMessageId: media.receiptMsgId,
};
}
if (shouldSkipBuyButton) return undefined;
return {
type: 'buy',
text,
@ -1155,9 +1171,11 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
type: 'unsupported',
text,
};
});
}).filter(Boolean);
});
if (markup.every((row) => !row.length)) return undefined;
return {
[replyMarkup instanceof GramJs.ReplyKeyboardMarkup ? 'keyboardButtons' : 'inlineButtons']: markup,
...(replyMarkup instanceof GramJs.ReplyKeyboardMarkup && {
@ -1368,6 +1386,21 @@ function buildUploadingMedia(
};
}
export function buildApiMessageExtendedMediaPreview(
preview: GramJs.MessageExtendedMediaPreview,
): ApiMessageExtendedMediaPreview {
const {
w, h, thumb, videoDuration,
} = preview;
return {
width: w,
height: h,
duration: videoDuration,
thumbnail: thumb ? buildApiThumbnailFromStripped([thumb]) : undefined,
};
}
export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined {
if (!document) return undefined;

View File

@ -353,7 +353,7 @@ export function isMessageWithMedia(message: GramJs.Message | GramJs.UpdateServic
media instanceof GramJs.MessageMediaGame
&& (media.game.document instanceof GramJs.Document || media.game.photo instanceof GramJs.Photo)
) || (
media instanceof GramJs.MessageMediaInvoice && media.photo
media instanceof GramJs.MessageMediaInvoice && (media.photo || media.extendedMedia)
)
);
}

View File

@ -26,37 +26,48 @@ export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) {
export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) {
const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`;
localDb.messages[messageFullId] = message;
if (message instanceof GramJs.Message) {
if (message.media instanceof GramJs.MessageMediaDocument
&& message.media.document instanceof GramJs.Document
let mockMessage = message;
if (message instanceof GramJs.Message
&& message.media instanceof GramJs.MessageMediaInvoice
&& message.media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
mockMessage = new GramJs.Message({
...message,
media: message.media.extendedMedia.media,
});
}
localDb.messages[messageFullId] = mockMessage;
if (mockMessage instanceof GramJs.Message) {
if (mockMessage.media instanceof GramJs.MessageMediaDocument
&& mockMessage.media.document instanceof GramJs.Document
) {
localDb.documents[String(message.media.document.id)] = message.media.document;
localDb.documents[String(mockMessage.media.document.id)] = mockMessage.media.document;
}
if (message.media instanceof GramJs.MessageMediaWebPage
&& message.media.webpage instanceof GramJs.WebPage
&& message.media.webpage.document instanceof GramJs.Document
if (mockMessage.media instanceof GramJs.MessageMediaWebPage
&& mockMessage.media.webpage instanceof GramJs.WebPage
&& mockMessage.media.webpage.document instanceof GramJs.Document
) {
localDb.documents[String(message.media.webpage.document.id)] = message.media.webpage.document;
localDb.documents[String(mockMessage.media.webpage.document.id)] = mockMessage.media.webpage.document;
}
if (message.media instanceof GramJs.MessageMediaGame) {
if (message.media.game.document instanceof GramJs.Document) {
localDb.documents[String(message.media.game.document.id)] = message.media.game.document;
if (mockMessage.media instanceof GramJs.MessageMediaGame) {
if (mockMessage.media.game.document instanceof GramJs.Document) {
localDb.documents[String(mockMessage.media.game.document.id)] = mockMessage.media.game.document;
}
addPhotoToLocalDb(message.media.game.photo);
addPhotoToLocalDb(mockMessage.media.game.photo);
}
if (message.media instanceof GramJs.MessageMediaInvoice
&& message.media.photo) {
localDb.webDocuments[String(message.media.photo.url)] = message.media.photo;
if (mockMessage.media instanceof GramJs.MessageMediaInvoice
&& mockMessage.media.photo) {
localDb.webDocuments[String(mockMessage.media.photo.url)] = mockMessage.media.photo;
}
}
if (message instanceof GramJs.MessageService && 'photo' in message.action) {
addPhotoToLocalDb(message.action.photo);
if (mockMessage instanceof GramJs.MessageService && 'photo' in mockMessage.action) {
addPhotoToLocalDb(mockMessage.action.photo);
}
}
@ -90,6 +101,24 @@ export function addEntitiesWithPhotosToLocalDb(entities: (GramJs.TypeUser | Gram
});
}
export function swapLocalInvoiceMedia(
chatId: string, messageId: number, extendedMedia: GramJs.TypeMessageExtendedMedia,
) {
const localMessage = localDb.messages[`${chatId}-${messageId}`];
if (!(localMessage instanceof GramJs.Message) || !localMessage.media) return;
if (extendedMedia instanceof GramJs.MessageExtendedMediaPreview) {
if (!(localMessage.media instanceof GramJs.MessageMediaInvoice)) {
return;
}
localMessage.media.extendedMedia = extendedMedia;
}
if (extendedMedia instanceof GramJs.MessageExtendedMedia) {
localMessage.media = extendedMedia.media;
}
}
export function serializeBytes(value: Buffer) {
return String.fromCharCode(...value);
}

View File

@ -29,7 +29,7 @@ export {
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
closePoll,
closePoll, fetchExtendedMedia,
} from './messages';
export {

View File

@ -1122,6 +1122,18 @@ export async function loadPollOptionResults({
};
}
export async function fetchExtendedMedia({
chat, ids,
} : {
chat: ApiChat;
ids: number[];
}) {
await invokeRequest(new GramJs.messages.GetExtendedMedia({
peer: buildInputPeer(chat.id, chat.accessHash),
id: ids,
}));
}
export async function forwardMessages({
fromChat,
toChat,

View File

@ -1,6 +1,8 @@
import type { GroupCallConnectionData } from '../../lib/secret-sauce';
import { Api as GramJs, connection } from '../../lib/gramjs';
import type { ApiMessage, ApiUpdateConnectionStateType, OnApiUpdate } from '../types';
import type {
ApiMessage, ApiMessageExtendedMediaPreview, ApiUpdateConnectionStateType, OnApiUpdate,
} from '../types';
import { pick } from '../../util/iteratees';
import {
@ -14,6 +16,7 @@ import {
buildApiMessageFromNotification,
buildMessageDraft,
buildMessageReactions,
buildApiMessageExtendedMediaPreview,
} from './apiBuilders/messages';
import {
buildChatMember,
@ -40,6 +43,7 @@ import {
resolveMessageApiChatId,
serializeBytes,
log,
swapLocalInvoiceMedia,
} from './helpers';
import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc';
import { buildApiPhoto } from './apiBuilders/common';
@ -309,6 +313,30 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
chatId: getApiChatIdFromMtpPeer(update.peer),
reactions: buildMessageReactions(update.reactions),
});
} else if (update instanceof GramJs.UpdateMessageExtendedMedia) {
let media: ApiMessage['content'] | undefined;
if (update.extendedMedia instanceof GramJs.MessageExtendedMedia) {
media = buildMessageMediaContent(update.extendedMedia.media);
}
let preview: ApiMessageExtendedMediaPreview | undefined;
if (update.extendedMedia instanceof GramJs.MessageExtendedMediaPreview) {
preview = buildApiMessageExtendedMediaPreview(update.extendedMedia);
}
if (!media && !preview) return;
const chatId = getApiChatIdFromMtpPeer(update.peer);
swapLocalInvoiceMedia(chatId, update.msgId, update.extendedMedia);
onUpdate({
'@type': 'updateMessageExtendedMedia',
id: update.msgId,
chatId,
media,
preview,
});
} else if (update instanceof GramJs.UpdateDeleteMessages) {
onUpdate({
'@type': 'deleteMessages',

View File

@ -187,10 +187,18 @@ export interface ApiInvoice {
isTest?: boolean;
isRecurring?: boolean;
recurringTermsUrl?: string;
extendedMedia?: ApiMessageExtendedMediaPreview;
maxTipAmount?: number;
suggestedTipAmounts?: number[];
}
export interface ApiMessageExtendedMediaPreview {
width?: number;
height?: number;
thumbnail?: ApiThumbnail;
duration?: number;
}
export interface ApiPaymentCredentials {
id: string;
title: string;
@ -406,6 +414,7 @@ export interface ApiMessage {
isSilent?: boolean;
seenByUserIds?: string[];
isProtected?: boolean;
isForwardingAllowed?: boolean;
transcriptionId?: string;
isTranscriptionError?: boolean;
emojiOnlyCount?: number;

View File

@ -13,7 +13,14 @@ import type {
ApiChatFolder,
} from './chats';
import type {
ApiFormattedText, ApiMessage, ApiPhoto, ApiPoll, ApiReactions, ApiStickerSet, ApiThreadInfo,
ApiFormattedText,
ApiMessage,
ApiMessageExtendedMediaPreview,
ApiPhoto,
ApiPoll,
ApiReactions,
ApiStickerSet,
ApiThreadInfo,
} from './messages';
import type {
ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus,
@ -309,6 +316,14 @@ export type ApiUpdateMessageReactions = {
reactions: ApiReactions;
};
export type ApiUpdateMessageExtendedMedia = {
'@type': 'updateMessageExtendedMedia';
id: number;
chatId: string;
media?: ApiMessage['content'];
preview?: ApiMessageExtendedMediaPreview;
};
export type ApiDeleteContact = {
'@type': 'deleteContact';
id: string;
@ -556,7 +571,8 @@ export type ApiUpdate = (
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
ApiUpdateMessageExtendedMedia
);
export type OnApiUpdate = (update: ApiUpdate) => void;

BIN
src/assets/turbulence.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -81,7 +81,7 @@ function getAvailableHeight(isGif?: boolean, aspectRatio?: number) {
return 27 * REM;
}
function calculateDimensionsForMessageMedia({
export function calculateDimensionsForMessageMedia({
width,
height,
fromOwnMessage,

View File

@ -320,7 +320,6 @@ const MediaViewer: FC<StateProps> = ({
onForward={handleForward}
zoomLevelChange={zoomLevelChange}
setZoomLevelChange={setZoomLevelChange}
isAvatar={Boolean(avatarOwner)}
/>
<ReportModal
isOpen={isReportModalOpen}

View File

@ -1,4 +1,3 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo,
useCallback,
@ -6,21 +5,27 @@ import React, {
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiMessage } from '../../api/types';
import type { MessageListType } from '../../global/types';
import type { MenuItemProps } from '../ui/MenuItem';
import {
selectIsDownloading,
selectIsMessageProtected,
selectAllowedMessageActions,
selectCurrentMessageList,
selectIsChatProtected,
} from '../../global/selectors';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { getMessageMediaFormat, getMessageMediaHash } from '../../global/helpers';
import useLang from '../../hooks/useLang';
import useMediaWithLoadProgress from '../../hooks/useMediaWithLoadProgress';
import useFlag from '../../hooks/useFlag';
import {
selectIsDownloading, selectIsMessageProtected, selectAllowedMessageActions, selectCurrentMessageList,
} from '../../global/selectors';
import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu';
import type { MenuItemProps } from '../ui/MenuItem';
import MenuItem from '../ui/MenuItem';
import ProgressSpinner from '../ui/ProgressSpinner';
import DeleteMessageModal from '../common/DeleteMessageModal';
@ -30,6 +35,7 @@ import './MediaViewerActions.scss';
type StateProps = {
isDownloading: boolean;
isProtected?: boolean;
isChatProtected?: boolean;
canDelete?: boolean;
messageListType?: MessageListType;
};
@ -40,7 +46,6 @@ type OwnProps = {
zoomLevelChange: number;
message?: ApiMessage;
fileName?: string;
isAvatar?: boolean;
canReport?: boolean;
onReport: NoneToVoidFunction;
onCloseMediaViewer: NoneToVoidFunction;
@ -53,17 +58,17 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
isVideo,
message,
fileName,
isAvatar,
isChatProtected,
isDownloading,
isProtected,
canReport,
zoomLevelChange,
canDelete,
messageListType,
onReport,
onCloseMediaViewer,
zoomLevelChange,
setZoomLevelChange,
canDelete,
onForward,
messageListType,
setZoomLevelChange,
}) => {
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(false);
@ -148,7 +153,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
if (IS_SINGLE_COLUMN_LAYOUT) {
const menuItems: MenuItemProps[] = [];
if (!isAvatar && !isProtected) {
if (!message?.isForwardingAllowed && !isChatProtected) {
menuItems.push({
icon: 'forward',
onClick: onForward,
@ -227,7 +232,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
return (
<div className="MediaViewerActions">
{!isAvatar && !isProtected && (
{message?.isForwardingAllowed && !isChatProtected && (
<Button
round
size="smaller"
@ -306,12 +311,14 @@ export default memo(withGlobal<OwnProps>(
const { threadId } = selectCurrentMessageList(global) || {};
const isDownloading = message ? selectIsDownloading(global, message) : false;
const isProtected = selectIsMessageProtected(global, message);
const isChatProtected = message && selectIsChatProtected(global, message?.chatId);
const { canDelete } = (threadId && message && selectAllowedMessageActions(global, message, threadId)) || {};
const messageListType = currentMessageList?.type;
return {
isDownloading,
isProtected,
isChatProtected,
canDelete,
messageListType,
};

View File

@ -14,7 +14,7 @@ import {
selectActiveDownloadIds,
selectAllowedMessageActions,
selectChat,
selectCurrentMessageList, selectIsCurrentUserPremium,
selectCurrentMessageList, selectIsChatProtected, selectIsCurrentUserPremium,
selectIsMessageProtected,
selectIsPremiumPurchaseBlocked,
selectMessageCustomEmojiSets,
@ -535,6 +535,7 @@ export default memo(withGlobal<OwnProps>(
&& !areReactionsEmpty(message.reactions) && message.reactions.canSeeList;
const canRemoveReaction = isPrivate && message.reactions?.results?.some((l) => l.isChosen);
const isProtected = selectIsMessageProtected(global, message);
const isChatProtected = selectIsChatProtected(global, message.chatId);
const canCopyNumber = Boolean(message.content.contact);
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
@ -554,11 +555,11 @@ export default memo(withGlobal<OwnProps>(
canDelete,
canReport,
canEdit: !isPinned && canEdit,
canForward: !isProtected && !isScheduled && canForward,
canForward: message.isForwardingAllowed && !isChatProtected && !isScheduled && canForward,
canFaveSticker: !isScheduled && canFaveSticker,
canUnfaveSticker: !isScheduled && canUnfaveSticker,
canCopy: canCopyNumber || (!isProtected && canCopy),
canCopyLink: !isProtected && !isScheduled && canCopyLink,
canCopyLink: !isScheduled && canCopyLink,
canSelect,
canDownload: !isProtected && canDownload,
canSaveGif: !isProtected && canSaveGif,

View File

@ -0,0 +1,117 @@
.root {
position: relative;
z-index: 0;
overflow: hidden;
cursor: pointer;
}
.dots {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
--background-url: url('../../../assets/turbulence.png');
--background-size: 256px;
background: rgba(0, 0, 0, 0.3) var(--background-url);
background-size: var(--background-size) var(--background-size);
z-index: 1;
--x-direction: var(--background-size);
--y-direction: 0;
animation: 10s linear infinite dots;
&.highres {
--background-url: url('../../../assets/turbulence_2x.png');
--background-size: 128px;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: var(--background-url);
background-size: var(--background-size) var(--background-size);
--x-direction: 0;
--y-direction: var(--background-size);
animation: 10s linear -4s infinite dots;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: var(--background-url);
background-size: var(--background-size) var(--background-size);
--x-direction: calc(-1 * var(--background-size));
--y-direction: calc(-1 * var(--background-size));
animation: 10s linear -8s infinite dots;
}
}
.duration {
z-index: 2;
position: absolute;
top: 0.25rem;
left: 0.25rem;
white-space: nowrap;
font-size: 0.75rem;
padding: 0 0.375rem;
background-color: rgba(0, 0, 0, 0.3);
color: #FFFFFF;
border-radius: 0.5rem;
}
.buy {
display: flex;
align-items: center;
gap: 0.375rem;
z-index: 2;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 0.375rem 0.625rem;
background-color: rgba(0, 0, 0, 0.5);
color: #FFFFFF;
border-radius: 1rem;
white-space: nowrap;
}
.lock {
margin-bottom: 1px;
}
.canvas {
display: block;
max-width: 100%;
}
@keyframes dots {
0% {
background-position: 0 0;
opacity: 1;
}
50% {
opacity: 0.75;
}
100% {
background-position: var(--x-direction) var(--y-direction);
opacity: 1;
}
}

View File

@ -0,0 +1,78 @@
import React, { memo, useCallback } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { FC } from '../../../lib/teact/teact';
import type { ApiMessage } from '../../../api/types';
import { getMessageInvoice } from '../../../global/helpers';
import { formatCurrency } from '../../../util/formatCurrency';
import { formatMediaDuration } from '../../../util/dateFormat';
import buildClassName from '../../../util/buildClassName';
import { DPR } from '../../../util/environment';
import useLang from '../../../hooks/useLang';
import useCanvasBlur from '../../../hooks/useCanvasBlur';
import useInterval from '../../../hooks/useInterval';
import styles from './InvoiceMediaPreview.module.scss';
type OwnProps = {
message: ApiMessage;
lastSyncTime?: number;
};
const POLLING_INTERVAL = 30000;
const BLUR_RADIUS = 25;
const InvoiceMediaPreview: FC<OwnProps> = ({
message,
lastSyncTime,
}) => {
const { openInvoice, loadExtendedMedia } = getActions();
const lang = useLang();
const invoice = getMessageInvoice(message);
const { chatId, id } = message;
const refreshExtendedMedia = useCallback(() => {
loadExtendedMedia({ chatId, ids: [id] });
}, [chatId, id, loadExtendedMedia]);
useInterval(refreshExtendedMedia, lastSyncTime ? POLLING_INTERVAL : undefined);
const {
amount,
currency,
extendedMedia,
} = invoice!;
const {
width, height, thumbnail, duration,
} = extendedMedia!;
const canvasRef = useCanvasBlur(thumbnail?.dataUri, false, undefined, BLUR_RADIUS, width, height);
const handleClick = useCallback(() => {
openInvoice({
chatId,
messageId: id,
});
}, [chatId, id, openInvoice]);
return (
<div
className={buildClassName(styles.root, 'media-inner')}
onClick={handleClick}
>
<canvas ref={canvasRef} className={styles.canvas} width={width} height={height} />
<div className={buildClassName(styles.dots, DPR > 1 && styles.highres)} />
{Boolean(duration) && <div className={styles.duration}>{formatMediaDuration(duration)}</div>}
<div className={styles.buy}>
<i className={buildClassName('icon-lock', styles.lock)} />
{lang('Checkout.PayPrice', formatCurrency(amount, currency))}
</div>
</div>
);
};
export default memo(InvoiceMediaPreview);

View File

@ -56,6 +56,7 @@ import {
selectAnimatedEmoji,
selectLocalAnimatedEmoji,
selectIsCurrentUserPremium,
selectIsChatProtected,
} from '../../../global/selectors';
import {
getMessageContent,
@ -79,7 +80,7 @@ import buildClassName from '../../../util/buildClassName';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import { renderMessageText } from '../../common/helpers/renderMessageText';
import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
import { calculateDimensionsForMessageMedia, ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
import { buildContentClassName } from './helpers/buildContentClassName';
import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions';
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
@ -113,6 +114,7 @@ import Contact from './Contact';
import Poll from './Poll';
import WebPage from './WebPage';
import Invoice from './Invoice';
import InvoiceMediaPreview from './InvoiceMediaPreview';
import Location from './Location';
import Game from './Game';
import Album from './Album';
@ -172,6 +174,7 @@ type StateProps = {
uploadProgress?: number;
isInDocumentGroup: boolean;
isProtected?: boolean;
isChatProtected?: boolean;
isFocused?: boolean;
focusDirection?: FocusDirection;
noFocusHighlight?: boolean;
@ -263,6 +266,7 @@ const Message: FC<OwnProps & StateProps> = ({
uploadProgress,
isInDocumentGroup,
isProtected,
isChatProtected,
isFocused,
focusDirection,
noFocusHighlight,
@ -371,7 +375,7 @@ const Message: FC<OwnProps & StateProps> = ({
!(isContextMenuShown || isInSelectMode || isForwarding)
&& !isInDocumentGroupNotLast
);
const canForward = isChannel && !isScheduled && !isProtected;
const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected;
const canFocus = Boolean(isPinnedList
|| (forwardInfo
&& (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat)
@ -579,7 +583,7 @@ const Message: FC<OwnProps & StateProps> = ({
}, [isAlbum, isOwn, asForwarded, noAvatars, album]);
const extraPadding = asForwarded ? 28 : 0;
if (!isAlbum && (photo || video)) {
if (!isAlbum && (photo || video || invoice?.extendedMedia)) {
let width: number | undefined;
if (photo) {
width = calculateMediaDimensions(message, noAvatars).width;
@ -589,6 +593,17 @@ const Message: FC<OwnProps & StateProps> = ({
} else {
width = calculateMediaDimensions(message, noAvatars).width;
}
} else if (invoice?.extendedMedia && (
invoice.extendedMedia.width && invoice.extendedMedia.height
)) {
const { width: previewWidth, height: previewHeight } = invoice.extendedMedia;
width = calculateDimensionsForMessageMedia({
width: previewWidth,
height: previewHeight,
fromOwnMessage: isOwn,
isForwarded: isForwarding,
noAvatars,
}).width;
}
if (width) {
@ -840,6 +855,12 @@ const Message: FC<OwnProps & StateProps> = ({
lastSyncTime={lastSyncTime}
/>
)}
{invoice?.extendedMedia && (
<InvoiceMediaPreview
message={message}
lastSyncTime={lastSyncTime}
/>
)}
{withVoiceTranscription && (
<p
@ -877,7 +898,7 @@ const Message: FC<OwnProps & StateProps> = ({
theme={theme}
/>
)}
{invoice && (
{invoice && !invoice.extendedMedia && (
<Invoice
message={message}
shouldAffectAppendix={hasCustomAppendix}
@ -1176,6 +1197,7 @@ export default memo(withGlobal<OwnProps>(
replyMessageSender,
isInDocumentGroup,
isProtected: selectIsMessageProtected(global, message),
isChatProtected: selectIsChatProtected(global, chatId),
isFocused,
isForwarding,
reactionMessage,

View File

@ -36,7 +36,7 @@ export function buildContentClassName(
} = getMessageContent(message);
const classNames = ['message-content'];
const isMedia = photo || video || location;
const isMedia = photo || video || location || invoice?.extendedMedia;
const hasText = text || location?.type === 'venue' || isGeoLiveActive;
const isMediaWithNoText = isMedia && !hasText;
const isViaBot = Boolean(message.viaBotId);
@ -87,7 +87,7 @@ export function buildContentClassName(
}
}
if (invoice) {
if (invoice && !invoice.extendedMedia) {
classNames.push('invoice');
}

View File

@ -608,6 +608,14 @@ addActionHandler('loadPollOptionResults', (global, actions, payload) => {
void loadPollOptionResults(chat, messageId, option, offset, limit, shouldResetVoters);
});
addActionHandler('loadExtendedMedia', (global, actions, payload) => {
const { chatId, ids } = payload;
const chat = selectChat(global, chatId);
if (chat) {
void callApi('fetchExtendedMedia', { chat, ids });
}
});
addActionHandler('forwardMessages', (global, action, payload) => {
const {
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions,

View File

@ -511,6 +511,37 @@ addActionHandler('apiUpdate', (global, actions, update) => {
break;
}
case 'updateMessageExtendedMedia': {
const {
chatId, id, media, preview,
} = update;
const message = selectChatMessage(global, chatId, id);
const chat = selectChat(global, update.chatId);
if (!chat || !message) return;
if (preview) {
if (!message.content.invoice) return;
setGlobal(updateChatMessage(global, chatId, id, {
content: {
...message.content,
invoice: {
...message.content.invoice,
extendedMedia: preview,
},
},
}));
} else if (media) {
setGlobal(updateChatMessage(global, chatId, id, {
content: {
...media,
},
}));
}
break;
}
case 'updateTranscribedAudio': {
const { transcriptionId, text, isPending } = update;

View File

@ -166,7 +166,7 @@ export function getMessageSummaryDescription(
}
if (invoice) {
summary = `${lang('PaymentInvoice')}: ${invoice.text}`;
summary = invoice.extendedMedia ? invoice.title : `${lang('PaymentInvoice')}: ${invoice.text}`;
}
if (text) {

View File

@ -901,7 +901,11 @@ export function selectLastServiceNotification(global: GlobalState) {
}
export function selectIsMessageProtected(global: GlobalState, message?: ApiMessage) {
return message ? message.isProtected || selectChat(global, message.chatId)?.isProtected : false;
return Boolean(message && (message.isProtected || selectIsChatProtected(global, message.chatId)));
}
export function selectIsChatProtected(global: GlobalState, chatId: string) {
return selectChat(global, chatId)?.isProtected || false;
}
export function selectHasProtectedMessage(global: GlobalState, chatId: string, messageIds?: number[]) {

View File

@ -790,6 +790,11 @@ export interface ActionPayloads {
messageId: number;
};
loadExtendedMedia: {
chatId: string;
ids: number[];
};
// Media Viewer & Audio Player
openMediaViewer: {
chatId?: string;

View File

@ -7,7 +7,14 @@ import { IS_CANVAS_FILTER_SUPPORTED } from '../util/environment';
const RADIUS = 2;
const ITERATIONS = 2;
export default function useCanvasBlur(dataUri?: string, isDisabled = false, withRaf?: boolean) {
export default function useCanvasBlur(
dataUri?: string,
isDisabled = false,
withRaf?: boolean,
radius = RADIUS,
preferredWidth?: number,
preferredHeight?: number,
) {
// eslint-disable-next-line no-null/no-null
const canvasRef = useRef<HTMLCanvasElement>(null);
const forceUpdate = useForceUpdate();
@ -22,19 +29,19 @@ export default function useCanvasBlur(dataUri?: string, isDisabled = false, with
const img = new Image();
const processBlur = () => {
canvas.width = img.width;
canvas.height = img.height;
canvas.width = preferredWidth || img.width;
canvas.height = preferredHeight || img.height;
const ctx = canvas.getContext('2d', { alpha: false })!;
if (IS_CANVAS_FILTER_SUPPORTED) {
ctx.filter = `blur(${RADIUS}px)`;
ctx.filter = `blur(${radius}px)`;
}
ctx.drawImage(img, -RADIUS * 2, -RADIUS * 2, canvas.width + RADIUS * 4, canvas.height + RADIUS * 4);
ctx.drawImage(img, -radius * 2, -radius * 2, canvas.width + radius * 4, canvas.height + radius * 4);
if (!IS_CANVAS_FILTER_SUPPORTED) {
fastBlur(ctx, 0, 0, canvas.width, canvas.height, RADIUS, ITERATIONS);
fastBlur(ctx, 0, 0, canvas.width, canvas.height, radius, ITERATIONS);
}
};
@ -47,7 +54,7 @@ export default function useCanvasBlur(dataUri?: string, isDisabled = false, with
};
img.src = dataUri;
}, [canvasRef, dataUri, forceUpdate, isDisabled, withRaf]);
}, [canvasRef, dataUri, forceUpdate, isDisabled, preferredHeight, preferredWidth, withRaf, radius]);
return canvasRef;
}

View File

@ -1,6 +1,6 @@
const api = require('./api');
const LAYER = 145;
const LAYER = 146;
const tlobjects = {};
for (const tl of Object.values(api)) {

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
inputMediaInvoice#d9799874 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string = InputMedia;
inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia;
inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
@ -99,7 +99,7 @@ messageMediaDocument#9cb070d7 flags:# nopremium:flags.3?true document:flags.0?Do
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;
messageMediaGame#fdb19008 game:Game = MessageMedia;
messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia;
messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia;
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
@ -314,6 +314,7 @@ updateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update;
updateRecentEmojiStatuses#30f443db = Update;
updateRecentReactions#6f7863f4 = Update;
updateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update;
updateMessageExtendedMedia#5a73a98c peer:Peer msg_id:int extended_media:MessageExtendedMedia = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference;
updates.difference#f49ca0 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> state:updates.State = updates.Difference;
@ -1042,6 +1043,8 @@ account.emailVerified#2b96cd1b email:string = account.EmailVerified;
account.emailVerifiedLogin#e1bb0d61 email:string sent_code:auth.SentCode = account.EmailVerified;
premiumSubscriptionOption#b6f11ebe flags:# current:flags.1?true can_purchase_upgrade:flags.2?true months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumSubscriptionOption;
sendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer;
messageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia;
messageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia;
---functions---
initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X;
invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;
@ -1223,6 +1226,7 @@ messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.Transcrib
messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;
messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;
messages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers;
messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;

View File

@ -157,6 +157,7 @@
"messages.hideAllChatJoinRequests",
"messages.toggleNoForwards",
"messages.saveDefaultSendAs",
"messages.getExtendedMedia",
"updates.getState",
"updates.getDifference",
"updates.getChannelDifference",

View File

@ -38,7 +38,7 @@ inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string pro
inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia;
inputMediaGame#d33f43f3 id:InputGame = InputMedia;
inputMediaInvoice#d9799874 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string = InputMedia;
inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia;
inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;
inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;
inputMediaDice#e66fbf7b emoticon:string = InputMedia;
@ -124,7 +124,7 @@ messageMediaDocument#9cb070d7 flags:# nopremium:flags.3?true document:flags.0?Do
messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia;
messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;
messageMediaGame#fdb19008 game:Game = MessageMedia;
messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia;
messageMediaInvoice#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia;
messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;
messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;
messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia;
@ -367,6 +367,7 @@ updateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update;
updateRecentEmojiStatuses#30f443db = Update;
updateRecentReactions#6f7863f4 = Update;
updateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update;
updateMessageExtendedMedia#5a73a98c peer:Peer msg_id:int extended_media:MessageExtendedMedia = Update;
updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;
@ -1414,6 +1415,9 @@ premiumSubscriptionOption#b6f11ebe flags:# current:flags.1?true can_purchase_upg
sendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer;
messageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia;
messageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia;
---functions---
invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;
@ -1727,6 +1731,7 @@ messages.reportReaction#3f64c076 peer:InputPeer id:int reaction_peer:InputPeer =
messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions;
messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions;
messages.clearRecentReactions#9dfeefb4 = Bool;
messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;
updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
@ -1889,4 +1894,4 @@ stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
// LAYER 145
// LAYER 146