Introduce Sponsored Messages (#1605)
This commit is contained in:
parent
1072c61a70
commit
af289a81f5
@ -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) {
|
||||
|
||||
@ -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) : [];
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 }),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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"
|
||||
|
||||
18
src/components/middle/message/SponsoredMessage.scss
Normal file
18
src/components/middle/message/SponsoredMessage.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
129
src/components/middle/message/SponsoredMessage.tsx
Normal file
129
src/components/middle/message/SponsoredMessage.tsx
Normal 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));
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@ export const INITIAL_STATE: GlobalState = {
|
||||
messages: {
|
||||
byChatId: {},
|
||||
messageLists: [],
|
||||
sponsoredByChatId: {},
|
||||
},
|
||||
|
||||
groupCalls: {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user