Sponsored messages

This commit is contained in:
morethanwords 2022-01-15 03:20:59 +04:00
parent a490f29c40
commit c44bba5560
11 changed files with 155 additions and 28 deletions

View File

@ -194,6 +194,7 @@ export default class ChatBubbles {
private getSponsoredMessagePromise: Promise<void>; private getSponsoredMessagePromise: Promise<void>;
private previousStickyDate: HTMLElement; private previousStickyDate: HTMLElement;
sponsoredMessage: import("/Users/kuzmenko/Documents/projects/tweb/src/layer").SponsoredMessage.sponsoredMessage;
constructor( constructor(
private chat: Chat, private chat: Chat,
@ -713,9 +714,22 @@ export default class ChatBubbles {
this.viewsObserver = new IntersectionObserver((entries) => { this.viewsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => { entries.forEach(entry => {
if(entry.isIntersecting) { if(entry.isIntersecting) {
this.viewsMids.add(+(entry.target as HTMLElement).dataset.mid); const mid = +(entry.target as HTMLElement).dataset.mid;
this.viewsObserver.unobserve(entry.target); this.viewsObserver.unobserve(entry.target);
this.sendViewCountersDebounced();
if(mid) {
this.viewsMids.add(mid);
this.sendViewCountersDebounced();
} else {
const {sponsoredMessage} = this;
if(sponsoredMessage && sponsoredMessage.random_id) {
delete sponsoredMessage.random_id;
this.chat.apiManager.invokeApiSingle('channels.viewSponsoredMessage', {
channel: this.appChatsManager.getChannelInput(this.peerId.toChatId()),
random_id: sponsoredMessage.random_id
});
}
}
} }
}); });
}); });
@ -1946,6 +1960,7 @@ export default class ChatBubbles {
this.onAnimateLadder = undefined; this.onAnimateLadder = undefined;
this.resolveLadderAnimation = undefined; this.resolveLadderAnimation = undefined;
this.emptyPlaceholderMid = undefined; this.emptyPlaceholderMid = undefined;
this.sponsoredMessage = undefined;
this.scrollingToBubble = undefined; this.scrollingToBubble = undefined;
////console.timeEnd('appImManager cleanup'); ////console.timeEnd('appImManager cleanup');
@ -3869,9 +3884,9 @@ export default class ChatBubbles {
const elements: (Node | string)[] = []; const elements: (Node | string)[] = [];
const isBot = this.appPeersManager.isBot(this.peerId); const isBot = this.appPeersManager.isBot(this.peerId);
if(isSponsored) { if(isSponsored) {
let text: LangPackKey, mid: number, callback: () => void; let text: LangPackKey, mid: number, startParam: string, callback: () => void;
const sponsoredMessage = (message as Message.message).sponsoredMessage; const sponsoredMessage = this.sponsoredMessage = (message as Message.message).sponsoredMessage;
const peerId = this.appPeersManager.getPeerId(sponsoredMessage.from_id); const peerId = this.appPeersManager.getPeerId(sponsoredMessage.from_id);
// const peer = this.appPeersManager.getPeer(peerId); // const peer = this.appPeersManager.getPeer(peerId);
if(sponsoredMessage.channel_post) { if(sponsoredMessage.channel_post) {
@ -3879,6 +3894,7 @@ export default class ChatBubbles {
mid = this.appMessagesIdsManager.generateMessageId(sponsoredMessage.channel_post); mid = this.appMessagesIdsManager.generateMessageId(sponsoredMessage.channel_post);
} else if(sponsoredMessage.start_param) { } else if(sponsoredMessage.start_param) {
text = 'Chat.Message.ViewBot'; text = 'Chat.Message.ViewBot';
startParam = sponsoredMessage.start_param;
} else { } else {
text = this.appPeersManager.isAnyGroup(peerId) ? 'Chat.Message.ViewGroup' : 'Chat.Message.ViewChannel'; text = this.appPeersManager.isAnyGroup(peerId) ? 'Chat.Message.ViewGroup' : 'Chat.Message.ViewChannel';
} }
@ -3886,7 +3902,8 @@ export default class ChatBubbles {
callback = () => { callback = () => {
rootScope.dispatchEvent('history_focus', { rootScope.dispatchEvent('history_focus', {
peerId, peerId,
mid mid,
startParam
}); });
}; };
@ -3894,6 +3911,8 @@ export default class ChatBubbles {
text text
}); });
this.viewsObserver.observe(button);
if(callback) { if(callback) {
attachClickEvent(button, callback); attachClickEvent(button, callback);
} }
@ -3996,7 +4015,7 @@ export default class ChatBubbles {
return; return;
} */ } */
if(side === 'bottom' && this.appPeersManager.isBroadcast(this.peerId) && false) { if(side === 'bottom' && this.appPeersManager.isBroadcast(this.peerId)/* && false */) {
const {mid} = this.generateLocalMessageId(SPONSORED_MESSAGE_ID_OFFSET); const {mid} = this.generateLocalMessageId(SPONSORED_MESSAGE_ID_OFFSET);
if(value) { if(value) {
const middleware = this.getMiddleware(() => { const middleware = this.getMiddleware(() => {
@ -4008,6 +4027,12 @@ export default class ChatBubbles {
}, {cacheSeconds: 300}).then(sponsoredMessages => { }, {cacheSeconds: 300}).then(sponsoredMessages => {
if(!middleware()) return; if(!middleware()) return;
forEachReverse(sponsoredMessages.messages, (message, idx, arr) => {
if(message.chat_invite || message.chat_invite_hash) {
arr.splice(idx, 1);
}
});
this.appUsersManager.saveApiUsers(sponsoredMessages.users); this.appUsersManager.saveApiUsers(sponsoredMessages.users);
this.appChatsManager.saveApiChats(sponsoredMessages.chats); this.appChatsManager.saveApiChats(sponsoredMessages.chats);

View File

@ -19,7 +19,7 @@ import PopupPinMessage from "../popups/unpinMessage";
import { copyTextToClipboard } from "../../helpers/clipboard"; import { copyTextToClipboard } from "../../helpers/clipboard";
import PopupSendNow from "../popups/sendNow"; import PopupSendNow from "../popups/sendNow";
import { toast } from "../toast"; import { toast } from "../toast";
import I18n, { LangPackKey } from "../../lib/langPack"; import I18n, { i18n, LangPackKey } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName"; import findUpClassName from "../../helpers/dom/findUpClassName";
import { cancelEvent } from "../../helpers/dom/cancelEvent"; import { cancelEvent } from "../../helpers/dom/cancelEvent";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent"; import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
@ -27,9 +27,10 @@ import isSelectionEmpty from "../../helpers/dom/isSelectionEmpty";
import { Message, Poll, Chat as MTChat, MessageMedia } from "../../layer"; import { Message, Poll, Chat as MTChat, MessageMedia } from "../../layer";
import PopupReportMessages from "../popups/reportMessages"; import PopupReportMessages from "../popups/reportMessages";
import assumeType from "../../helpers/assumeType"; import assumeType from "../../helpers/assumeType";
import PopupSponsored from "../popups/sponsored";
export default class ChatContextMenu { export default class ChatContextMenu {
private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true})[]; private buttons: (ButtonMenuItemOptions & {verify: () => boolean, notDirect?: () => boolean, withSelection?: true, isSponsored?: true})[];
private element: HTMLElement; private element: HTMLElement;
private isSelectable: boolean; private isSelectable: boolean;
@ -77,6 +78,7 @@ export default class ChatContextMenu {
let mid = +bubble.dataset.mid; let mid = +bubble.dataset.mid;
if(!mid) return; if(!mid) return;
const isSponsored = mid < 0;
this.isSelectable = this.chat.selection.canSelectBubble(bubble); this.isSelectable = this.chat.selection.canSelectBubble(bubble);
this.peerId = this.chat.peerId; this.peerId = this.chat.peerId;
//this.msgID = msgID; //this.msgID = msgID;
@ -90,6 +92,10 @@ export default class ChatContextMenu {
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты // * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
if(chat.selection.isSelecting && !contentWrapper) { if(chat.selection.isSelecting && !contentWrapper) {
if(isSponsored) {
return;
}
const mids = this.chat.getMidsByMid(mid); const mids = this.chat.getMidsByMid(mid);
if(mids.length > 1) { if(mids.length > 1) {
const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ? const selectedMid = this.chat.selection.isMidSelected(this.peerId, mid) ?
@ -111,22 +117,28 @@ export default class ChatContextMenu {
this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid); this.isSelected = this.chat.selection.isMidSelected(this.peerId, this.mid);
this.message = this.chat.getMessage(this.mid); this.message = this.chat.getMessage(this.mid);
this.noForwards = !this.appMessagesManager.canForward(this.message); if(isSponsored) {
this.buttons.forEach(button => {
button.element.classList.toggle('hide', !button.isSponsored);
});
} else {
this.noForwards = !this.appMessagesManager.canForward(this.message);
this.buttons.forEach(button => { this.buttons.forEach(button => {
let good: boolean; let good: boolean;
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) { //if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
if(chat.selection.isSelecting && !button.withSelection) { if(chat.selection.isSelecting && !button.withSelection) {
good = false; good = false;
} else { } else {
good = contentWrapper || IS_TOUCH_SUPPORTED || true ? good = contentWrapper || IS_TOUCH_SUPPORTED || true ?
button.verify() : button.verify() :
button.notDirect && button.verify() && button.notDirect(); button.notDirect && button.verify() && button.notDirect();
} }
button.element.classList.toggle('hide', !good); button.element.classList.toggle('hide', !good);
}); });
}
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right'; const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
//bubble.parentElement.append(this.element); //bubble.parentElement.append(this.element);
@ -361,6 +373,14 @@ export default class ChatContextMenu {
verify: () => this.isSelected && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'), verify: () => this.isSelected && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'),
notDirect: () => true, notDirect: () => true,
withSelection: true withSelection: true
}, {
icon: 'info',
text: 'Chat.Message.Sponsored.What',
onClick: () => {
new PopupSponsored();
},
verify: () => false,
isSponsored: true
}]; }];
this.element = ButtonMenu(this.buttons, this.chat.bubbles.listenerSetter); this.element = ButtonMenu(this.buttons, this.chat.bubbles.listenerSetter);

View File

@ -820,7 +820,7 @@ export default class ChatSelection extends AppSelection {
} }
public canSelectBubble(bubble: HTMLElement) { public canSelectBubble(bubble: HTMLElement) {
return !bubble.classList.contains('service') && !bubble.classList.contains('is-sending') && !bubble.classList.contains('bubble-first'); return !bubble.classList.contains('service') && !bubble.classList.contains('is-sending') && !bubble.classList.contains('bubble-first') && bubble.parentElement.classList.contains('bubbles-date-group');
} }
protected onToggleSelection = (forwards: boolean, animate: boolean) => { protected onToggleSelection = (forwards: boolean, animate: boolean) => {

View File

@ -27,6 +27,8 @@ export type PopupPeerOptions = PopupOptions & Partial<{
checkboxes: Array<PopupPeerCheckboxOptions> checkboxes: Array<PopupPeerCheckboxOptions>
}>; }>;
export default class PopupPeer extends PopupElement { export default class PopupPeer extends PopupElement {
protected description: HTMLParagraphElement;
constructor(private className: string, options: PopupPeerOptions = {}) { constructor(private className: string, options: PopupPeerOptions = {}) {
super('popup-peer' + (className ? ' ' + className : ''), options.buttons && addCancelButton(options.buttons), {overlayClosable: true, ...options}); super('popup-peer' + (className ? ' ' + className : ''), options.buttons && addCancelButton(options.buttons), {overlayClosable: true, ...options});
@ -48,7 +50,7 @@ export default class PopupPeer extends PopupElement {
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
if(options.descriptionLangKey || options.description) { if(options.descriptionLangKey || options.description) {
const p = document.createElement('p'); const p = this.description = document.createElement('p');
p.classList.add('popup-description'); p.classList.add('popup-description');
if(options.descriptionLangKey) p.append(i18n(options.descriptionLangKey, options.descriptionLangArgs)); if(options.descriptionLangKey) p.append(i18n(options.descriptionLangKey, options.descriptionLangArgs));
else if(options.description) p.innerHTML = options.description; else if(options.description) p.innerHTML = options.description;

View File

@ -0,0 +1,42 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import I18n, { i18n } from "../../lib/langPack";
import Scrollable from "../scrollable";
import PopupPeer from "./peer";
export default class PopupSponsored extends PopupPeer {
constructor() {
super('popup-sponsored', {
titleLangKey: 'Chat.Message.Sponsored.What',
descriptionLangKey: 'Chat.Message.Ad.Text',
descriptionLangArgs: [i18n('Chat.Message.Sponsored.Link')],
buttons: [{
langKey: 'OK',
isCancel: true
}, {
langKey: 'Chat.Message.Ad.ReadMore',
callback: () => {
window.open(I18n.format('Chat.Message.Sponsored.Link', true));
},
isCancel: true
}]
});
const scrollable = new Scrollable(undefined);
scrollable.onAdditionalScroll = () => {
scrollable.container.classList.toggle('scrolled-top', !scrollable.scrollTop);
scrollable.container.classList.toggle('scrolled-bottom', scrollable.isScrolledDown);
};
this.description.replaceWith(scrollable.container);
scrollable.container.append(this.description);
scrollable.container.classList.add('scrolled-top');
this.show();
}
}

View File

@ -744,9 +744,13 @@ const lang = {
"one_value": "Do you want to unpin %d message in this chat?", "one_value": "Do you want to unpin %d message in this chat?",
"other_value": "Do you want to unpin all %d messages in this chat?" "other_value": "Do you want to unpin all %d messages in this chat?"
}, },
"Chat.Message.Ad.Text": "Unlike other apps, Telegram never uses your private data to target ads. Sponsored messages on Telegram are based solely on the topic of the public channels in which they are shown. This means that no user data is mined or analyzed to display ads, and every user viewing a channel on Telegram sees the same sponsored messages.\n\nUnlike other apps, Telegram doesn't track whether you tapped on a sponsored message and doesn't profile you based on your activity. We also prevent external links in sponsored messages to ensure that third parties cant spy on our users. We believe that everyone has the right to privacy, and technological platforms should respect that.\n\nTelegram offers a free and unlimited service to hundreds of millions of users, which involves significant server and traffic costs. In order to remain independent and stay true to its values, Telegram developed a paid tool to promote messages with user privacy in mind. We welcome responsible advertisers at:\n\n%@\n\nSponsored Messages are currently in test mode. Once they are fully launched and allow Telegram to cover its basic costs, we will start sharing ad revenue with the owners of public channels in which sponsored messages are displayed.\n\nOnline ads should no longer be synonymous with abuse of user privacy. Let us redefine how a tech company should operate together.",
"Chat.Message.Ad.ReadMore": "Read More",
"Chat.Message.ViewChannel": "VIEW CHANNEL", "Chat.Message.ViewChannel": "VIEW CHANNEL",
"Chat.Message.ViewBot": "VIEW BOT", "Chat.Message.ViewBot": "VIEW BOT",
"Chat.Message.ViewGroup": "VIEW GROUP", "Chat.Message.ViewGroup": "VIEW GROUP",
"Chat.Message.Sponsored.What": "What are sponsored messages?",
"Chat.Message.Sponsored.Link": "https://promote.telegram.org",
"ChatList.Context.Mute": "Mute", "ChatList.Context.Mute": "Mute",
"ChatList.Context.Unmute": "Unmute", "ChatList.Context.Unmute": "Unmute",
"ChatList.Context.Pin": "Pin", "ChatList.Context.Pin": "Pin",

View File

@ -207,7 +207,7 @@ export class AppImManager {
}); });
rootScope.addEventListener('history_focus', (e) => { rootScope.addEventListener('history_focus', (e) => {
let {peerId, threadId, mid} = e; let {peerId, threadId, mid, startParam} = e;
if(threadId) threadId = appMessagesIdsManager.generateMessageId(threadId); if(threadId) threadId = appMessagesIdsManager.generateMessageId(threadId);
if(mid) mid = appMessagesIdsManager.generateMessageId(mid); // because mid can come from notification, i.e. server message id if(mid) mid = appMessagesIdsManager.generateMessageId(mid); // because mid can come from notification, i.e. server message id
@ -215,7 +215,8 @@ export class AppImManager {
peerId, peerId,
lastMsgId: mid, lastMsgId: mid,
type: threadId ? 'discussion' : undefined, type: threadId ? 'discussion' : undefined,
threadId threadId,
startParam
}); });
}); });

View File

@ -69,7 +69,7 @@ export type BroadcastEvents = {
'history_delete': {peerId: PeerId, msgs: Set<number>}, 'history_delete': {peerId: PeerId, msgs: Set<number>},
'history_forbidden': PeerId, 'history_forbidden': PeerId,
'history_reload': PeerId, 'history_reload': PeerId,
'history_focus': {peerId: PeerId, threadId?: number, mid?: number}, 'history_focus': {peerId: PeerId, threadId?: number, mid?: number, startParam?: string},
//'history_request': void, //'history_request': void,
'message_edit': {storage: MessagesStorage, peerId: PeerId, mid: number}, 'message_edit': {storage: MessagesStorage, peerId: PeerId, mid: number},

View File

@ -1412,6 +1412,12 @@ $background-transition-total-time: #{$input-transition-time - $background-transi
> .bubble.is-in { > .bubble.is-in {
// margin-left: 0; // margin-left: 0;
width: 100%; width: 100%;
@include respond-to(medium-screens) {
.bubble-content-wrapper {
max-width: 85%;
}
}
} }
} }

View File

@ -0,0 +1,26 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
.popup-sponsored {
.scrollable-y {
position: relative;
max-height: 25rem;
margin: 0 -1.5rem;
width: calc(100% + 3rem);
padding: .5rem 1.5rem;
user-select: text;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
&:not(.scrolled-top) {
border-top: 1px solid var(--border-color);
}
&:not(.scrolled-bottom) {
border-bottom: 1px solid var(--border-color);
}
}
}

View File

@ -313,6 +313,7 @@ $chat-input-inner-padding-handhelds: .25rem;
@import "partials/popups/reportMessages"; @import "partials/popups/reportMessages";
@import "partials/popups/groupCall"; @import "partials/popups/groupCall";
@import "partials/popups/call"; @import "partials/popups/call";
@import "partials/popups/sponsored";
@import "partials/pages/pages"; @import "partials/pages/pages";
@import "partials/pages/authCode"; @import "partials/pages/authCode";