Introduce Sponsored Messages (#1605)
This commit is contained in:
parent
1072c61a70
commit
af289a81f5
@ -22,12 +22,14 @@ import {
|
|||||||
ApiThreadInfo,
|
ApiThreadInfo,
|
||||||
ApiInvoice,
|
ApiInvoice,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
|
ApiSponsoredMessage,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DELETED_COMMENTS_CHANNEL_ID,
|
DELETED_COMMENTS_CHANNEL_ID,
|
||||||
LOCAL_MESSAGE_ID_BASE,
|
LOCAL_MESSAGE_ID_BASE,
|
||||||
SERVICE_NOTIFICATIONS_USER_ID,
|
SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
|
SPONSORED_MESSAGE_CACHE_MS,
|
||||||
SUPPORTED_IMAGE_CONTENT_TYPES,
|
SUPPORTED_IMAGE_CONTENT_TYPES,
|
||||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||||
VIDEO_MOV_TYPE,
|
VIDEO_MOV_TYPE,
|
||||||
@ -37,8 +39,8 @@ import { buildStickerFromDocument } from './symbols';
|
|||||||
import { buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromStripped } from './common';
|
import { buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromStripped } from './common';
|
||||||
import { interpolateArray } from '../../../util/waveform';
|
import { interpolateArray } from '../../../util/waveform';
|
||||||
import { buildPeer } from '../gramjsBuilders';
|
import { buildPeer } from '../gramjsBuilders';
|
||||||
import { addPhotoToLocalDb, resolveMessageApiChatId } from '../helpers';
|
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
||||||
|
|
||||||
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
|
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
|
||||||
const INPUT_WAVEFORM_LENGTH = 63;
|
const INPUT_WAVEFORM_LENGTH = 63;
|
||||||
@ -50,6 +52,30 @@ export function setMessageBuilderCurrentUserId(_currentUserId: string) {
|
|||||||
currentUserId = _currentUserId;
|
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 {
|
export function buildApiMessage(mtpMessage: GramJs.TypeMessage): ApiMessage | undefined {
|
||||||
const chatId = resolveMessageApiChatId(mtpMessage);
|
const chatId = resolveMessageApiChatId(mtpMessage);
|
||||||
if (
|
if (
|
||||||
@ -493,7 +519,7 @@ export function buildPoll(poll: GramJs.Poll, pollResults: GramJs.PollResults): A
|
|||||||
const { id, answers: rawAnswers } = poll;
|
const { id, answers: rawAnswers } = poll;
|
||||||
const answers = rawAnswers.map((answer) => ({
|
const answers = rawAnswers.map((answer) => ({
|
||||||
text: answer.text,
|
text: answer.text,
|
||||||
option: String.fromCharCode(...answer.option),
|
option: serializeBytes(answer.option),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -539,7 +565,7 @@ export function buildPollResults(pollResults: GramJs.PollResults): ApiPoll['resu
|
|||||||
}) => ({
|
}) => ({
|
||||||
isChosen: chosen,
|
isChosen: chosen,
|
||||||
isCorrect: correct,
|
isCorrect: correct,
|
||||||
option: String.fromCharCode(...option),
|
option: serializeBytes(option),
|
||||||
votersCount: voters,
|
votersCount: voters,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -775,7 +801,7 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi
|
|||||||
value = button.url;
|
value = button.url;
|
||||||
} else if (button instanceof GramJs.KeyboardButtonCallback) {
|
} else if (button instanceof GramJs.KeyboardButtonCallback) {
|
||||||
type = 'callback';
|
type = 'callback';
|
||||||
value = String(button.data);
|
value = serializeBytes(button.data);
|
||||||
} else if (button instanceof GramJs.KeyboardButtonRequestPoll) {
|
} else if (button instanceof GramJs.KeyboardButtonRequestPoll) {
|
||||||
type = 'requestPoll';
|
type = 'requestPoll';
|
||||||
} else if (button instanceof GramJs.KeyboardButtonBuy) {
|
} else if (button instanceof GramJs.KeyboardButtonBuy) {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
} from '../../types';
|
} from '../../types';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
|
import { deserializeBytes } from '../helpers';
|
||||||
|
|
||||||
const CHANNEL_ID_MIN_LENGTH = 11; // Example: -1000000000
|
const CHANNEL_ID_MIN_LENGTH = 11; // Example: -1000000000
|
||||||
|
|
||||||
@ -166,7 +167,9 @@ export function buildInputPoll(pollParams: ApiNewPoll, randomId: BigInt.BigInteg
|
|||||||
id: randomId,
|
id: randomId,
|
||||||
publicVoters: summary.isPublic,
|
publicVoters: summary.isPublic,
|
||||||
question: summary.question,
|
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,
|
quiz: summary.quiz,
|
||||||
multipleChoice: summary.multipleChoice,
|
multipleChoice: summary.multipleChoice,
|
||||||
});
|
});
|
||||||
@ -175,7 +178,7 @@ export function buildInputPoll(pollParams: ApiNewPoll, randomId: BigInt.BigInteg
|
|||||||
return new GramJs.InputMediaPoll({ poll });
|
return new GramJs.InputMediaPoll({ poll });
|
||||||
}
|
}
|
||||||
|
|
||||||
const correctAnswers = quiz.correctAnswers.map((key) => Buffer.from(key));
|
const correctAnswers = quiz.correctAnswers.map(deserializeBytes);
|
||||||
const { solution } = quiz;
|
const { solution } = quiz;
|
||||||
const solutionEntities = quiz.solutionEntities ? quiz.solutionEntities.map(buildMtpMessageEntity) : [];
|
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 { buildApiUser } from '../apiBuilders/users';
|
||||||
import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots';
|
import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb } from '../helpers';
|
import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers';
|
||||||
|
|
||||||
export function init() {
|
export function init() {
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@ export function answerCallbackButton(
|
|||||||
return invokeRequest(new GramJs.messages.GetBotCallbackAnswer({
|
return invokeRequest(new GramJs.messages.GetBotCallbackAnswer({
|
||||||
peer: buildInputPeer(chatId, accessHash),
|
peer: buildInputPeer(chatId, accessHash),
|
||||||
msgId: messageId,
|
msgId: messageId,
|
||||||
data: Buffer.from(data),
|
data: deserializeBytes(data),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export {
|
|||||||
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages, sendMessageAction, fetchSeenBy,
|
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
MESSAGE_DELETED,
|
MESSAGE_DELETED,
|
||||||
ApiGlobalMessageSearchType,
|
ApiGlobalMessageSearchType,
|
||||||
ApiReportReason,
|
ApiReportReason,
|
||||||
|
ApiSponsoredMessage,
|
||||||
ApiSendMessageAction,
|
ApiSendMessageAction,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ import {
|
|||||||
buildLocalMessage,
|
buildLocalMessage,
|
||||||
buildWebPage,
|
buildWebPage,
|
||||||
buildLocalForwardedMessage,
|
buildLocalForwardedMessage,
|
||||||
|
buildApiSponsoredMessage,
|
||||||
} from '../apiBuilders/messages';
|
} from '../apiBuilders/messages';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
import {
|
import {
|
||||||
@ -50,7 +52,7 @@ import {
|
|||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
import { fetchFile } from '../../../util/files';
|
import { fetchFile } from '../../../util/files';
|
||||||
import { addMessageToLocalDb, resolveMessageApiChatId } from '../helpers';
|
import { addMessageToLocalDb, deserializeBytes, resolveMessageApiChatId } from '../helpers';
|
||||||
import { interpolateArray } from '../../../util/waveform';
|
import { interpolateArray } from '../../../util/waveform';
|
||||||
import { requestChatUpdate } from './chats';
|
import { requestChatUpdate } from './chats';
|
||||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||||
@ -994,7 +996,7 @@ export async function sendPollVote({
|
|||||||
await invokeRequest(new GramJs.messages.SendVote({
|
await invokeRequest(new GramJs.messages.SendVote({
|
||||||
peer: buildInputPeer(id, accessHash),
|
peer: buildInputPeer(id, accessHash),
|
||||||
msgId: messageId,
|
msgId: messageId,
|
||||||
options: options.map((option) => Buffer.from(option)),
|
options: options.map(deserializeBytes),
|
||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1013,7 +1015,7 @@ export async function loadPollOptionResults({
|
|||||||
const result = await invokeRequest(new GramJs.messages.GetPollVotes({
|
const result = await invokeRequest(new GramJs.messages.GetPollVotes({
|
||||||
peer: buildInputPeer(id, accessHash),
|
peer: buildInputPeer(id, accessHash),
|
||||||
id: messageId,
|
id: messageId,
|
||||||
...(option && { option: Buffer.from(option) }),
|
...(option && { option: deserializeBytes(option) }),
|
||||||
...(offset && { offset }),
|
...(offset && { offset }),
|
||||||
...(limit && { limit }),
|
...(limit && { limit }),
|
||||||
}));
|
}));
|
||||||
@ -1143,7 +1145,7 @@ export async function sendScheduledMessages({ chat, ids }: { chat: ApiChat; ids:
|
|||||||
|
|
||||||
function updateLocalDb(result: (
|
function updateLocalDb(result: (
|
||||||
GramJs.messages.MessagesSlice | GramJs.messages.Messages | GramJs.messages.ChannelMessages |
|
GramJs.messages.MessagesSlice | GramJs.messages.Messages | GramJs.messages.ChannelMessages |
|
||||||
GramJs.messages.DiscussionMessage
|
GramJs.messages.DiscussionMessage | GramJs.messages.SponsoredMessages
|
||||||
)) {
|
)) {
|
||||||
result.users.forEach((user) => {
|
result.users.forEach((user) => {
|
||||||
if (user instanceof GramJs.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;
|
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,
|
addEntitiesWithPhotosToLocalDb,
|
||||||
addPhotoToLocalDb,
|
addPhotoToLocalDb,
|
||||||
resolveMessageApiChatId,
|
resolveMessageApiChatId,
|
||||||
|
serializeBytes,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc';
|
import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc';
|
||||||
import { buildApiPhoto } from './apiBuilders/common';
|
import { buildApiPhoto } from './apiBuilders/common';
|
||||||
@ -466,7 +467,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
'@type': 'updateMessagePollVote',
|
'@type': 'updateMessagePollVote',
|
||||||
pollId: String(update.pollId),
|
pollId: String(update.pollId),
|
||||||
userId: buildApiPeerId(update.userId, 'user'),
|
userId: buildApiPeerId(update.userId, 'user'),
|
||||||
options: update.options.map((option) => String.fromCharCode(...option)),
|
options: update.options.map(serializeBytes),
|
||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdateChannelMessageViews) {
|
} else if (update instanceof GramJs.UpdateChannelMessageViews) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
|
|||||||
@ -280,6 +280,18 @@ export interface ApiThreadInfo {
|
|||||||
|
|
||||||
export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed';
|
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 {
|
export interface ApiKeyboardButton {
|
||||||
type: 'command' | 'url' | 'callback' | 'requestPoll' | 'buy' | 'NOT_SUPPORTED';
|
type: 'command' | 'url' | 'callback' | 'requestPoll' | 'buy' | 'NOT_SUPPORTED';
|
||||||
text: string;
|
text: string;
|
||||||
|
|||||||
@ -89,6 +89,7 @@ type StateProps = {
|
|||||||
threadTopMessageId?: number;
|
threadTopMessageId?: number;
|
||||||
threadFirstMessageId?: number;
|
threadFirstMessageId?: number;
|
||||||
hasLinkedChat?: boolean;
|
hasLinkedChat?: boolean;
|
||||||
|
lastSyncTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const BOTTOM_THRESHOLD = 20;
|
const BOTTOM_THRESHOLD = 20;
|
||||||
@ -131,9 +132,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
botDescription,
|
botDescription,
|
||||||
threadTopMessageId,
|
threadTopMessageId,
|
||||||
hasLinkedChat,
|
hasLinkedChat,
|
||||||
|
lastSyncTime,
|
||||||
withBottomShift,
|
withBottomShift,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadViewportMessages, setScrollOffset } = getDispatch();
|
const { loadViewportMessages, setScrollOffset, loadSponsoredMessages } = getDispatch();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -168,6 +170,12 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
memoFirstUnreadIdRef.current = firstUnreadId;
|
memoFirstUnreadIdRef.current = firstUnreadId;
|
||||||
}, [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)
|
// Updated only once when messages are loaded (as we want the unread divider to keep its position)
|
||||||
useOnChange(() => {
|
useOnChange(() => {
|
||||||
if (areMessagesLoaded) {
|
if (areMessagesLoaded) {
|
||||||
@ -506,6 +514,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
) : ((messageIds && messageGroups) || lastMessage) ? (
|
) : ((messageIds && messageGroups) || lastMessage) ? (
|
||||||
<MessageListContent
|
<MessageListContent
|
||||||
|
chatId={chatId}
|
||||||
messageIds={messageIds || [lastMessage!.id]}
|
messageIds={messageIds || [lastMessage!.id]}
|
||||||
messageGroups={messageGroups || groupMessages([lastMessage!])}
|
messageGroups={messageGroups || groupMessages([lastMessage!])}
|
||||||
isViewportNewest={Boolean(isViewportNewest)}
|
isViewportNewest={Boolean(isViewportNewest)}
|
||||||
@ -595,6 +604,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
hasLinkedChat: chat.fullInfo && ('linkedChatId' in chat.fullInfo)
|
hasLinkedChat: chat.fullInfo && ('linkedChatId' in chat.fullInfo)
|
||||||
? Boolean(chat.fullInfo.linkedChatId)
|
? Boolean(chat.fullInfo.linkedChatId)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
lastSyncTime: global.lastSyncTime,
|
||||||
...(withLastMessageWhenPreloading && { lastMessage }),
|
...(withLastMessageWhenPreloading && { lastMessage }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -15,10 +15,12 @@ import useScrollHooks from './hooks/useScrollHooks';
|
|||||||
import useMessageObservers from './hooks/useMessageObservers';
|
import useMessageObservers from './hooks/useMessageObservers';
|
||||||
|
|
||||||
import Message from './message/Message';
|
import Message from './message/Message';
|
||||||
|
import SponsoredMessage from './message/SponsoredMessage';
|
||||||
import ActionMessage from './ActionMessage';
|
import ActionMessage from './ActionMessage';
|
||||||
import { getDispatch } from '../../lib/teact/teactn';
|
import { getDispatch } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
|
chatId: string;
|
||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
messageGroups: MessageDateGroup[];
|
messageGroups: MessageDateGroup[];
|
||||||
isViewportNewest: boolean;
|
isViewportNewest: boolean;
|
||||||
@ -45,6 +47,7 @@ interface OwnProps {
|
|||||||
const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
||||||
|
|
||||||
const MessageListContent: FC<OwnProps> = ({
|
const MessageListContent: FC<OwnProps> = ({
|
||||||
|
chatId,
|
||||||
messageIds,
|
messageIds,
|
||||||
messageGroups,
|
messageGroups,
|
||||||
isViewportNewest,
|
isViewportNewest,
|
||||||
@ -236,6 +239,7 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
<div className="messages-container" teactFastList>
|
<div className="messages-container" teactFastList>
|
||||||
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
|
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
|
||||||
{flatten(dateGroups)}
|
{flatten(dateGroups)}
|
||||||
|
{isViewportNewest && <SponsoredMessage key={chatId} chatId={chatId} containerRef={containerRef} />}
|
||||||
<div
|
<div
|
||||||
ref={forwardsTriggerRef}
|
ref={forwardsTriggerRef}
|
||||||
key="forwards-trigger"
|
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 TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
||||||
export const ALL_CHATS_PRELOAD_DISABLED = false;
|
export const ALL_CHATS_PRELOAD_DISABLED = false;
|
||||||
|
|
||||||
|
export const SPONSORED_MESSAGE_CACHE_MS = 300000; // 5 min
|
||||||
|
|
||||||
export const DEFAULT_VOLUME = 1;
|
export const DEFAULT_VOLUME = 1;
|
||||||
export const DEFAULT_PLAYBACK_RATE = 1;
|
export const DEFAULT_PLAYBACK_RATE = 1;
|
||||||
|
|
||||||
|
|||||||
@ -194,6 +194,10 @@ function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
if (!cached.users.statusesById) {
|
if (!cached.users.statusesById) {
|
||||||
cached.users.statusesById = {};
|
cached.users.statusesById = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.messages.sponsoredByChatId) {
|
||||||
|
cached.messages.sponsoredByChatId = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache() {
|
function updateCache() {
|
||||||
@ -311,6 +315,7 @@ function reduceMessages(global: GlobalState): GlobalState['messages'] {
|
|||||||
return {
|
return {
|
||||||
byChatId,
|
byChatId,
|
||||||
messageLists: [],
|
messageLists: [],
|
||||||
|
sponsoredByChatId: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
messages: {
|
messages: {
|
||||||
byChatId: {},
|
byChatId: {},
|
||||||
messageLists: [],
|
messageLists: [],
|
||||||
|
sponsoredByChatId: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
groupCalls: {
|
groupCalls: {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
ApiCountryCode,
|
ApiCountryCode,
|
||||||
ApiCountry,
|
ApiCountry,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
|
ApiSponsoredMessage,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import {
|
import {
|
||||||
FocusDirection,
|
FocusDirection,
|
||||||
@ -167,6 +168,7 @@ export type GlobalState = {
|
|||||||
poll?: ApiNewPoll;
|
poll?: ApiNewPoll;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
};
|
};
|
||||||
|
sponsoredByChatId: Record<string, ApiSponsoredMessage>;
|
||||||
};
|
};
|
||||||
|
|
||||||
groupCalls: {
|
groupCalls: {
|
||||||
@ -503,6 +505,7 @@ export type ActionTypes = (
|
|||||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||||
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
||||||
|
'loadSponsoredMessages' | 'viewSponsoredMessage' |
|
||||||
// downloads
|
// downloads
|
||||||
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||||
// scheduled messages
|
// scheduled messages
|
||||||
|
|||||||
@ -1117,6 +1117,8 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool
|
|||||||
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
||||||
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||||
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
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.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.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;
|
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.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
||||||
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||||
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
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.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.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;
|
payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
updateThreadInfos,
|
updateThreadInfos,
|
||||||
updateChat,
|
updateChat,
|
||||||
updateThreadUnreadFromForwardedMessage,
|
updateThreadUnreadFromForwardedMessage,
|
||||||
|
updateSponsoredMessage,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -55,6 +56,7 @@ import {
|
|||||||
selectScheduledMessage,
|
selectScheduledMessage,
|
||||||
selectNoWebPage,
|
selectNoWebPage,
|
||||||
selectFirstUnreadId,
|
selectFirstUnreadId,
|
||||||
|
selectSponsoredMessage,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { debounce, rafPromise } from '../../../util/schedulers';
|
import { debounce, rafPromise } from '../../../util/schedulers';
|
||||||
import { isServiceNotificationMessage } from '../../helpers';
|
import { isServiceNotificationMessage } from '../../helpers';
|
||||||
@ -1001,6 +1003,38 @@ async function loadScheduledHistory(chat: ApiChat) {
|
|||||||
setGlobal(global);
|
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) {
|
function countSortedIds(ids: number[], from: number, to: number) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
GlobalState, MessageList, MessageListType, Thread,
|
GlobalState, MessageList, MessageListType, Thread,
|
||||||
} from '../../global/types';
|
} 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 { FocusDirection } from '../../types';
|
||||||
|
|
||||||
import {
|
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(
|
export function updateFocusDirection(
|
||||||
global: GlobalState, direction?: FocusDirection,
|
global: GlobalState, direction?: FocusDirection,
|
||||||
): GlobalState {
|
): GlobalState {
|
||||||
|
|||||||
@ -855,3 +855,10 @@ export function selectLastServiceNotification(global: GlobalState) {
|
|||||||
|
|
||||||
return serviceNotifications.find(({ id }) => id === maxId);
|
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