Introduce Sponsored Messages (#1605)

This commit is contained in:
Alexander Zinchuk 2021-12-31 18:17:38 +01:00
parent 1072c61a70
commit af289a81f5
21 changed files with 332 additions and 17 deletions

View File

@ -22,12 +22,14 @@ import {
ApiThreadInfo,
ApiInvoice,
ApiGroupCall,
ApiSponsoredMessage,
} from '../../types';
import {
DELETED_COMMENTS_CHANNEL_ID,
LOCAL_MESSAGE_ID_BASE,
SERVICE_NOTIFICATIONS_USER_ID,
SPONSORED_MESSAGE_CACHE_MS,
SUPPORTED_IMAGE_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
VIDEO_MOV_TYPE,
@ -37,8 +39,8 @@ import { buildStickerFromDocument } from './symbols';
import { buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromStripped } from './common';
import { interpolateArray } from '../../../util/waveform';
import { buildPeer } from '../gramjsBuilders';
import { addPhotoToLocalDb, resolveMessageApiChatId } from '../helpers';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
const INPUT_WAVEFORM_LENGTH = 63;
@ -50,6 +52,30 @@ export function setMessageBuilderCurrentUserId(_currentUserId: string) {
currentUserId = _currentUserId;
}
export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): ApiSponsoredMessage | undefined {
const {
fromId, message, entities, startParam, channelPost, chatInvite, chatInviteHash, randomId,
} = mtpMessage;
const chatId = fromId ? getApiChatIdFromMtpPeer(fromId) : undefined;
const chatInviteTitle = chatInvite
? (chatInvite instanceof GramJs.ChatInvite
? chatInvite.title
: !(chatInvite.chat instanceof GramJs.ChatEmpty) ? chatInvite.chat.title : undefined)
: undefined;
return {
randomId: serializeBytes(randomId),
isBot: fromId ? isPeerUser(fromId) : false,
text: buildMessageTextContent(message, entities),
expiresAt: Math.round(Date.now() / 1000) + SPONSORED_MESSAGE_CACHE_MS,
...(chatId && { chatId }),
...(chatInviteHash && { chatInviteHash }),
...(chatInvite && { chatInviteTitle }),
...(startParam && { startParam }),
...(channelPost && { channelPostId: channelPost }),
};
}
export function buildApiMessage(mtpMessage: GramJs.TypeMessage): ApiMessage | undefined {
const chatId = resolveMessageApiChatId(mtpMessage);
if (
@ -493,7 +519,7 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A
const { id, answers: rawAnswers } = poll;
const answers = rawAnswers.map((answer) => ({
text: answer.text,
option: String.fromCharCode(...answer.option),
option: serializeBytes(answer.option),
}));
return {
@ -539,7 +565,7 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu
}) => ({
isChosen: chosen,
isCorrect: correct,
option: String.fromCharCode(...option),
option: serializeBytes(option),
votersCount: voters,
}));
@ -775,7 +801,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
value = button.url;
} else if (button instanceof GramJs.KeyboardButtonCallback) {
type = 'callback';
value = String(button.data);
value = serializeBytes(button.data);
} else if (button instanceof GramJs.KeyboardButtonRequestPoll) {
type = 'requestPoll';
} else if (button instanceof GramJs.KeyboardButtonBuy) {

View File

@ -19,6 +19,7 @@ import {
} from '../../types';
import localDb from '../localDb';
import { pick } from '../../../util/iteratees';
import { deserializeBytes } from '../helpers';
const CHANNEL_ID_MIN_LENGTH = 11; // Example: -1000000000
@ -166,7 +167,9 @@ export function buildInputPoll(pollParams: ApiNewPoll, randomId: BigInt.BigInteg
id: randomId,
publicVoters: summary.isPublic,
question: summary.question,
answers: summary.answers.map(({ text, option }) => new GramJs.PollAnswer({ text, option: Buffer.from(option) })),
answers: summary.answers.map(({ text, option }) => {
return new GramJs.PollAnswer({ text, option: deserializeBytes(option) });
}),
quiz: summary.quiz,
multipleChoice: summary.multipleChoice,
});
@ -175,7 +178,7 @@ export function buildInputPoll(pollParams: ApiNewPoll, randomId: BigInt.BigInteg
return new GramJs.InputMediaPoll({ poll });
}
const correctAnswers = quiz.correctAnswers.map((key) => Buffer.from(key));
const correctAnswers = quiz.correctAnswers.map(deserializeBytes);
const { solution } = quiz;
const solutionEntities = quiz.solutionEntities ? quiz.solutionEntities.map(buildMtpMessageEntity) : [];

View File

@ -65,3 +65,11 @@ export function addEntitiesWithPhotosToLocalDb(entities: (GramJs.TypeUser | Gram
}
});
}
export function serializeBytes(value: Buffer) {
return String.fromCharCode(...value);
}
export function deserializeBytes(value: string) {
return Buffer.from(value, 'binary');
}

View File

@ -9,7 +9,7 @@ import { buildInputPeer, generateRandomBigInt } from '../gramjsBuilders';
import { buildApiUser } from '../apiBuilders/users';
import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb } from '../helpers';
import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers';
export function init() {
}
@ -24,7 +24,7 @@ export function answerCallbackButton(
return invokeRequest(new GramJs.messages.GetBotCallbackAnswer({
peer: buildInputPeer(chatId, accessHash),
msgId: messageId,
data: Buffer.from(data),
data: deserializeBytes(data),
}));
}

View File

@ -22,7 +22,7 @@ export {
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
reportMessages, sendMessageAction, fetchSeenBy,
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage,
} from './messages';
export {

View File

@ -16,6 +16,7 @@ import {
MESSAGE_DELETED,
ApiGlobalMessageSearchType,
ApiReportReason,
ApiSponsoredMessage,
ApiSendMessageAction,
} from '../../types';
@ -32,6 +33,7 @@ import {
buildLocalMessage,
buildWebPage,
buildLocalForwardedMessage,
buildApiSponsoredMessage,
} from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users';
import {
@ -50,7 +52,7 @@ import {
import localDb from '../localDb';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { fetchFile } from '../../../util/files';
import { addMessageToLocalDb, resolveMessageApiChatId } from '../helpers';
import { addMessageToLocalDb, deserializeBytes, resolveMessageApiChatId } from '../helpers';
import { interpolateArray } from '../../../util/waveform';
import { requestChatUpdate } from './chats';
import { buildApiPeerId } from '../apiBuilders/peers';
@ -994,7 +996,7 @@ export async function sendPollVote({
await invokeRequest(new GramJs.messages.SendVote({
peer: buildInputPeer(id, accessHash),
msgId: messageId,
options: options.map((option) => Buffer.from(option)),
options: options.map(deserializeBytes),
}), true);
}
@ -1013,7 +1015,7 @@ export async function loadPollOptionResults({
const result = await invokeRequest(new GramJs.messages.GetPollVotes({
peer: buildInputPeer(id, accessHash),
id: messageId,
...(option && { option: Buffer.from(option) }),
...(option && { option: deserializeBytes(option) }),
...(offset && { offset }),
...(limit && { limit }),
}));
@ -1143,7 +1145,7 @@ export async function sendScheduledMessages({ chat, ids }: { chat: ApiChat; ids:
function updateLocalDb(result: (
GramJs.messages.MessagesSlice | GramJs.messages.Messages | GramJs.messages.ChannelMessages |
GramJs.messages.DiscussionMessage
GramJs.messages.DiscussionMessage | GramJs.messages.SponsoredMessages
)) {
result.users.forEach((user) => {
if (user instanceof GramJs.User) {
@ -1205,3 +1207,32 @@ export async function fetchSeenBy({ chat, messageId }: { chat: ApiChat; messageI
return result ? result.map(String) : undefined;
}
export async function fetchSponsoredMessages({ chat }: { chat: ApiChat }) {
const result = await invokeRequest(new GramJs.channels.GetSponsoredMessages({
channel: buildInputPeer(chat.id, chat.accessHash),
}));
if (!result || !result.messages.length) {
return undefined;
}
updateLocalDb(result);
const messages = result.messages.map(buildApiSponsoredMessage).filter<ApiSponsoredMessage>(Boolean as any);
const users = result.users.map(buildApiUser).filter<ApiUser>(Boolean as any);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter<ApiChat>(Boolean as any);
return {
messages,
users,
chats,
};
}
export async function viewSponsoredMessage({ chat, random }: { chat: ApiChat; random: string }) {
await invokeRequest(new GramJs.channels.ViewSponsoredMessage({
channel: buildInputPeer(chat.id, chat.accessHash),
randomId: deserializeBytes(random),
}));
}

View File

@ -36,6 +36,7 @@ import {
addEntitiesWithPhotosToLocalDb,
addPhotoToLocalDb,
resolveMessageApiChatId,
serializeBytes,
} from './helpers';
import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc';
import { buildApiPhoto } from './apiBuilders/common';
@ -466,7 +467,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
'@type': 'updateMessagePollVote',
pollId: String(update.pollId),
userId: buildApiPeerId(update.userId, 'user'),
options: update.options.map((option) => String.fromCharCode(...option)),
options: update.options.map(serializeBytes),
});
} else if (update instanceof GramJs.UpdateChannelMessageViews) {
onUpdate({

View File

@ -280,6 +280,18 @@ export interface ApiThreadInfo {
export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed';
export type ApiSponsoredMessage = {
chatId?: string;
randomId: string;
isBot?: boolean;
channelPostId?: number;
startParam?: string;
chatInviteHash?: string;
chatInviteTitle?: string;
text: ApiFormattedText;
expiresAt: number;
};
export interface ApiKeyboardButton {
type: 'command' | 'url' | 'callback' | 'requestPoll' | 'buy' | 'NOT_SUPPORTED';
text: string;

View File

@ -89,6 +89,7 @@ type StateProps = {
threadTopMessageId?: number;
threadFirstMessageId?: number;
hasLinkedChat?: boolean;
lastSyncTime?: number;
};
const BOTTOM_THRESHOLD = 20;
@ -131,9 +132,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
botDescription,
threadTopMessageId,
hasLinkedChat,
lastSyncTime,
withBottomShift,
}) => {
const { loadViewportMessages, setScrollOffset } = getDispatch();
const { loadViewportMessages, setScrollOffset, loadSponsoredMessages } = getDispatch();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
@ -168,6 +170,12 @@ const MessageList: FC<OwnProps & StateProps> = ({
memoFirstUnreadIdRef.current = firstUnreadId;
}, [firstUnreadId]);
useOnChange(() => {
if (isChannelChat && isReady && lastSyncTime) {
loadSponsoredMessages({ chatId });
}
}, [chatId, isReady, isChannelChat, lastSyncTime]);
// Updated only once when messages are loaded (as we want the unread divider to keep its position)
useOnChange(() => {
if (areMessagesLoaded) {
@ -506,6 +514,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
/>
) : ((messageIds && messageGroups) || lastMessage) ? (
<MessageListContent
chatId={chatId}
messageIds={messageIds || [lastMessage!.id]}
messageGroups={messageGroups || groupMessages([lastMessage!])}
isViewportNewest={Boolean(isViewportNewest)}
@ -595,6 +604,7 @@ export default memo(withGlobal<OwnProps>(
hasLinkedChat: chat.fullInfo && ('linkedChatId' in chat.fullInfo)
? Boolean(chat.fullInfo.linkedChatId)
: undefined,
lastSyncTime: global.lastSyncTime,
...(withLastMessageWhenPreloading && { lastMessage }),
};
},

View File

@ -15,10 +15,12 @@ import useScrollHooks from './hooks/useScrollHooks';
import useMessageObservers from './hooks/useMessageObservers';
import Message from './message/Message';
import SponsoredMessage from './message/SponsoredMessage';
import ActionMessage from './ActionMessage';
import { getDispatch } from '../../lib/teact/teactn';
interface OwnProps {
chatId: string;
messageIds: number[];
messageGroups: MessageDateGroup[];
isViewportNewest: boolean;
@ -45,6 +47,7 @@ interface OwnProps {
const UNREAD_DIVIDER_CLASS = 'unread-divider';
const MessageListContent: FC<OwnProps> = ({
chatId,
messageIds,
messageGroups,
isViewportNewest,
@ -236,6 +239,7 @@ const MessageListContent: FC<OwnProps> = ({
<div className="messages-container" teactFastList>
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
{flatten(dateGroups)}
{isViewportNewest && <SponsoredMessage key={chatId} chatId={chatId} containerRef={containerRef} />}
<div
ref={forwardsTriggerRef}
key="forwards-trigger"

View File

@ -0,0 +1,18 @@
.SponsoredMessage {
--border-top-left-radius: var(--border-radius-messages) !important;
--border-bottom-left-radius: var(--border-radius-messages) !important;
margin-top: -.5rem;
margin-bottom: .5rem;
&::before {
display: none;
}
&__button.secondary {
margin-top: .5rem;
border: 1px solid var(--color-primary);
border-radius: var(--border-radius-default-tiny);
color: var(--color-primary);
}
}

View File

@ -0,0 +1,129 @@
import { RefObject } from 'react';
import React, {
FC, memo, useEffect, useRef,
} from '../../../lib/teact/teact';
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
import { ApiChat, ApiSponsoredMessage, ApiUser } from '../../../api/types';
import { renderTextWithEntities } from '../../common/helpers/renderMessageText';
import { selectChat, selectSponsoredMessage, selectUser } from '../../../modules/selectors';
import { getChatTitle, getUserFullName } from '../../../modules/helpers';
import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import Button from '../../ui/Button';
import './SponsoredMessage.scss';
type OwnProps = {
chatId: string;
containerRef: RefObject<HTMLDivElement>;
};
type StateProps = {
message?: ApiSponsoredMessage;
bot?: ApiUser;
channel?: ApiChat;
};
const INTERSECTION_DEBOUNCE_MS = 200;
const SponsoredMessage: FC<OwnProps & StateProps> = ({
chatId,
message,
containerRef,
bot,
channel,
}) => {
const {
viewSponsoredMessage,
openChat,
openChatByInvite,
startBot,
focusMessage,
} = getDispatch();
const lang = useLang();
// eslint-disable-next-line no-null/no-null
const contentRef = useRef<HTMLDivElement>(null);
const shouldObserve = Boolean(message);
const {
observe: observeIntersection,
} = useIntersectionObserver({
rootRef: containerRef,
debounceMs: INTERSECTION_DEBOUNCE_MS,
threshold: 1,
});
useEffect(() => {
return shouldObserve ? observeIntersection(contentRef.current!, (target) => {
if (target.isIntersecting) {
viewSponsoredMessage({ chatId });
}
}) : undefined;
}, [chatId, shouldObserve, observeIntersection, viewSponsoredMessage]);
if (!message) {
return undefined;
}
const handleClick = () => {
if (message.chatInviteHash) {
openChatByInvite({ hash: message.chatInviteHash });
} else if (message.channelPostId) {
focusMessage({ chatId: message.chatId, messageId: message.channelPostId });
} else {
openChat({ id: message.chatId });
if (message.startParam) {
startBot({
botId: message.chatId,
param: message.startParam,
});
}
}
};
return (
<div className="SponsoredMessage Message open" key="sponsored-message">
<div className="message-content has-shadow has-solid-background" dir="auto">
<div className="content-inner" dir="auto">
<div className="message-title" dir="ltr">
{bot && renderText(getUserFullName(bot) || '')}
{channel && renderText(message.chatInviteTitle || getChatTitle(lang, channel, bot) || '')}
</div>
<p className="text-content with-meta" dir="auto" ref={contentRef}>
<span className="text-content-inner" dir="auto">
{renderTextWithEntities(message.text.text, message.text.entities)}
</span>
<span className="MessageMeta" dir="ltr">
<span className="message-signature">{lang('SponsoredMessage')}</span>
</span>
</p>
<Button color="secondary" size="tiny" ripple onClick={handleClick} className="SponsoredMessage__button">
{lang(message.isBot
? 'Conversation.ViewBot'
: (message.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel'))}
</Button>
</div>
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const message = selectSponsoredMessage(global, chatId);
const { chatId: fromChatId, isBot } = message || {};
return {
message,
bot: fromChatId && isBot ? selectUser(global, fromChatId) : undefined,
channel: !isBot && fromChatId ? selectChat(global, fromChatId) : undefined,
};
},
)(SponsoredMessage));

View File

@ -62,6 +62,8 @@ export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
export const ALL_CHATS_PRELOAD_DISABLED = false;
export const SPONSORED_MESSAGE_CACHE_MS = 300000; // 5 min
export const DEFAULT_VOLUME = 1;
export const DEFAULT_PLAYBACK_RATE = 1;

View File

@ -194,6 +194,10 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.users.statusesById) {
cached.users.statusesById = {};
}
if (!cached.messages.sponsoredByChatId) {
cached.messages.sponsoredByChatId = {};
}
}
function updateCache() {
@ -311,6 +315,7 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] {
return {
byChatId,
messageLists: [],
sponsoredByChatId: {},
};
}

View File

@ -43,6 +43,7 @@ export const INITIAL_STATE: GlobalState = {
messages: {
byChatId: {},
messageLists: [],
sponsoredByChatId: {},
},
groupCalls: {

View File

@ -23,6 +23,7 @@ import {
ApiCountryCode,
ApiCountry,
ApiGroupCall,
ApiSponsoredMessage,
} from '../api/types';
import {
FocusDirection,
@ -167,6 +168,7 @@ export type GlobalState = {
poll?: ApiNewPoll;
isSilent?: boolean;
};
sponsoredByChatId: Record<string, ApiSponsoredMessage>;
};
groupCalls: {
@ -503,6 +505,7 @@ export type ActionTypes = (
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
'loadSponsoredMessages' | 'viewSponsoredMessage' |
// downloads
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
// scheduled messages

View File

@ -1117,6 +1117,8 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool;
channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages;
payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm;
payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;

View File

@ -1118,6 +1118,8 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool;
channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages;
payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm;
payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;

View File

@ -34,6 +34,7 @@ import {
updateThreadInfos,
updateChat,
updateThreadUnreadFromForwardedMessage,
updateSponsoredMessage,
} from '../../reducers';
import {
selectChat,
@ -55,6 +56,7 @@ import {
selectScheduledMessage,
selectNoWebPage,
selectFirstUnreadId,
selectSponsoredMessage,
} from '../../selectors';
import { debounce, rafPromise } from '../../../util/schedulers';
import { isServiceNotificationMessage } from '../../helpers';
@ -1001,6 +1003,38 @@ async function loadScheduledHistory(chat: ApiChat) {
setGlobal(global);
}
addReducer('loadSponsoredMessages', (global, actions, payload) => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
(async () => {
const result = await callApi('fetchSponsoredMessages', { chat });
if (!result) {
return;
}
let newGlobal = updateSponsoredMessage(getGlobal(), chatId, result.messages[0]);
newGlobal = addUsers(newGlobal, buildCollectionByKey(result.users, 'id'));
newGlobal = addChats(newGlobal, buildCollectionByKey(result.chats, 'id'));
setGlobal(newGlobal);
})();
});
addReducer('viewSponsoredMessage', (global, actions, payload) => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
const message = selectSponsoredMessage(global, chatId);
if (!chat || !message) {
return;
}
void callApi('viewSponsoredMessage', { chat, random: message.randomId });
});
function countSortedIds(ids: number[], from: number, to: number) {
let count = 0;

View File

@ -1,7 +1,9 @@
import {
GlobalState, MessageList, MessageListType, Thread,
} from '../../global/types';
import { ApiMessage, ApiThreadInfo, MAIN_THREAD_ID } from '../../api/types';
import {
ApiMessage, ApiSponsoredMessage, ApiThreadInfo, MAIN_THREAD_ID,
} from '../../api/types';
import { FocusDirection } from '../../types';
import {
@ -445,6 +447,21 @@ export function updateFocusedMessage(
};
}
export function updateSponsoredMessage(
global: GlobalState, chatId: string, message: ApiSponsoredMessage,
): GlobalState {
return {
...global,
messages: {
...global.messages,
sponsoredByChatId: {
...global.messages.sponsoredByChatId,
[chatId]: message,
},
},
};
}
export function updateFocusDirection(
global: GlobalState, direction?: FocusDirection,
): GlobalState {

View File

@ -855,3 +855,10 @@ export function selectLastServiceNotification(global: GlobalState) {
return serviceNotifications.find(({ id }) => id === maxId);
}
export function selectSponsoredMessage(global: GlobalState, chatId: string) {
const chat = selectChat(global, chatId);
const message = chat && isChatChannel(chat) ? global.messages.sponsoredByChatId[chatId] : undefined;
return message && message.expiresAt >= Math.round(Date.now() / 1000) ? message : undefined;
}