diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 2a3698a05..31c14a211 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -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) { diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index acffdaab4..bbbb3a98c 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -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) : []; diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index 3f3f2ce6e..7f10b1123 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -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'); +} diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 8755469dd..14a793e65 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -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), })); } diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 1cdd0e34d..bc37674c3 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -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 { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 72036207e..991a39b2f 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -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(Boolean as any); + const users = result.users.map(buildApiUser).filter(Boolean as any); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(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), + })); +} diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index e13b85d6c..de57ebea1 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -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({ diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 983c8812a..86b5c5ea6 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -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; diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 5048e8b9a..0c1f8c3db 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -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 = ({ 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(null); @@ -168,6 +170,12 @@ const MessageList: FC = ({ 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 = ({ /> ) : ((messageIds && messageGroups) || lastMessage) ? ( ( hasLinkedChat: chat.fullInfo && ('linkedChatId' in chat.fullInfo) ? Boolean(chat.fullInfo.linkedChatId) : undefined, + lastSyncTime: global.lastSyncTime, ...(withLastMessageWhenPreloading && { lastMessage }), }; }, diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index a4c2d625d..f817e9602 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -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 = ({ + chatId, messageIds, messageGroups, isViewportNewest, @@ -236,6 +239,7 @@ const MessageListContent: FC = ({
{flatten(dateGroups)} + {isViewportNewest && }
; +}; + +type StateProps = { + message?: ApiSponsoredMessage; + bot?: ApiUser; + channel?: ApiChat; +}; + +const INTERSECTION_DEBOUNCE_MS = 200; + +const SponsoredMessage: FC = ({ + 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(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 ( +
+
+
+
+ {bot && renderText(getUserFullName(bot) || '')} + {channel && renderText(message.chatInviteTitle || getChatTitle(lang, channel, bot) || '')} +
+ +

+ + {renderTextWithEntities(message.text.text, message.text.entities)} + + + + {lang('SponsoredMessage')} + +

+ + +
+
+
+ ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/config.ts b/src/config.ts index fe1bddf61..7adf1ac83 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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; diff --git a/src/global/cache.ts b/src/global/cache.ts index 78a6de808..375280f59 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -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: {}, }; } diff --git a/src/global/initial.ts b/src/global/initial.ts index 8a1a9913e..55c325b62 100644 --- a/src/global/initial.ts +++ b/src/global/initial.ts @@ -43,6 +43,7 @@ export const INITIAL_STATE: GlobalState = { messages: { byChatId: {}, messageLists: [], + sponsoredByChatId: {}, }, groupCalls: { diff --git a/src/global/types.ts b/src/global/types.ts index b275d85ca..9ffdf1094 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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; }; 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 diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 590ec6e30..8c0ac1ce1 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1117,6 +1117,8 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = 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; diff --git a/src/lib/gramjs/tl/static/api.reduced.tl b/src/lib/gramjs/tl/static/api.reduced.tl index b64fe3c0e..f47eb1d93 100644 --- a/src/lib/gramjs/tl/static/api.reduced.tl +++ b/src/lib/gramjs/tl/static/api.reduced.tl @@ -1118,6 +1118,8 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = 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; diff --git a/src/modules/actions/api/messages.ts b/src/modules/actions/api/messages.ts index 544ab6d51..08741ca5f 100644 --- a/src/modules/actions/api/messages.ts +++ b/src/modules/actions/api/messages.ts @@ -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; diff --git a/src/modules/reducers/messages.ts b/src/modules/reducers/messages.ts index 0d84b7421..8e327615c 100644 --- a/src/modules/reducers/messages.ts +++ b/src/modules/reducers/messages.ts @@ -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 { diff --git a/src/modules/selectors/messages.ts b/src/modules/selectors/messages.ts index 7f31a10f4..826678089 100644 --- a/src/modules/selectors/messages.ts +++ b/src/modules/selectors/messages.ts @@ -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; +}