Sponsored messages

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

View File

@ -194,6 +194,7 @@ export default class ChatBubbles {
private getSponsoredMessagePromise: Promise<void>;
private previousStickyDate: HTMLElement;
sponsoredMessage: import("/Users/kuzmenko/Documents/projects/tweb/src/layer").SponsoredMessage.sponsoredMessage;
constructor(
private chat: Chat,
@ -713,9 +714,22 @@ export default class ChatBubbles {
this.viewsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
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.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.resolveLadderAnimation = undefined;
this.emptyPlaceholderMid = undefined;
this.sponsoredMessage = undefined;
this.scrollingToBubble = undefined;
////console.timeEnd('appImManager cleanup');
@ -3869,9 +3884,9 @@ export default class ChatBubbles {
const elements: (Node | string)[] = [];
const isBot = this.appPeersManager.isBot(this.peerId);
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 peer = this.appPeersManager.getPeer(peerId);
if(sponsoredMessage.channel_post) {
@ -3879,6 +3894,7 @@ export default class ChatBubbles {
mid = this.appMessagesIdsManager.generateMessageId(sponsoredMessage.channel_post);
} else if(sponsoredMessage.start_param) {
text = 'Chat.Message.ViewBot';
startParam = sponsoredMessage.start_param;
} else {
text = this.appPeersManager.isAnyGroup(peerId) ? 'Chat.Message.ViewGroup' : 'Chat.Message.ViewChannel';
}
@ -3886,7 +3902,8 @@ export default class ChatBubbles {
callback = () => {
rootScope.dispatchEvent('history_focus', {
peerId,
mid
mid,
startParam
});
};
@ -3894,6 +3911,8 @@ export default class ChatBubbles {
text
});
this.viewsObserver.observe(button);
if(callback) {
attachClickEvent(button, callback);
}
@ -3996,7 +4015,7 @@ export default class ChatBubbles {
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);
if(value) {
const middleware = this.getMiddleware(() => {
@ -4008,6 +4027,12 @@ export default class ChatBubbles {
}, {cacheSeconds: 300}).then(sponsoredMessages => {
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.appChatsManager.saveApiChats(sponsoredMessages.chats);

View File

@ -19,7 +19,7 @@ import PopupPinMessage from "../popups/unpinMessage";
import { copyTextToClipboard } from "../../helpers/clipboard";
import PopupSendNow from "../popups/sendNow";
import { toast } from "../toast";
import I18n, { LangPackKey } from "../../lib/langPack";
import I18n, { i18n, LangPackKey } from "../../lib/langPack";
import findUpClassName from "../../helpers/dom/findUpClassName";
import { cancelEvent } from "../../helpers/dom/cancelEvent";
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 PopupReportMessages from "../popups/reportMessages";
import assumeType from "../../helpers/assumeType";
import PopupSponsored from "../popups/sponsored";
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 isSelectable: boolean;
@ -77,6 +78,7 @@ export default class ChatContextMenu {
let mid = +bubble.dataset.mid;
if(!mid) return;
const isSponsored = mid < 0;
this.isSelectable = this.chat.selection.canSelectBubble(bubble);
this.peerId = this.chat.peerId;
//this.msgID = msgID;
@ -90,6 +92,10 @@ export default class ChatContextMenu {
// * если открыть контекстное меню для альбома не по бабблу, и последний элемент не выбран, чтобы показать остальные пункты
if(chat.selection.isSelecting && !contentWrapper) {
if(isSponsored) {
return;
}
const mids = this.chat.getMidsByMid(mid);
if(mids.length > 1) {
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.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 => {
let good: boolean;
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
if(chat.selection.isSelecting && !button.withSelection) {
good = false;
} else {
good = contentWrapper || IS_TOUCH_SUPPORTED || true ?
button.verify() :
button.notDirect && button.verify() && button.notDirect();
}
button.element.classList.toggle('hide', !good);
});
this.buttons.forEach(button => {
let good: boolean;
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
if(chat.selection.isSelecting && !button.withSelection) {
good = false;
} else {
good = contentWrapper || IS_TOUCH_SUPPORTED || true ?
button.verify() :
button.notDirect && button.verify() && button.notDirect();
}
button.element.classList.toggle('hide', !good);
});
}
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
//bubble.parentElement.append(this.element);
@ -361,6 +373,14 @@ export default class ChatContextMenu {
verify: () => this.isSelected && !this.chat.selection.selectionDeleteBtn.hasAttribute('disabled'),
notDirect: () => 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);

View File

@ -820,7 +820,7 @@ export default class ChatSelection extends AppSelection {
}
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) => {

View File

@ -27,6 +27,8 @@ export type PopupPeerOptions = PopupOptions & Partial<{
checkboxes: Array<PopupPeerCheckboxOptions>
}>;
export default class PopupPeer extends PopupElement {
protected description: HTMLParagraphElement;
constructor(private className: string, options: PopupPeerOptions = {}) {
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();
if(options.descriptionLangKey || options.description) {
const p = document.createElement('p');
const p = this.description = document.createElement('p');
p.classList.add('popup-description');
if(options.descriptionLangKey) p.append(i18n(options.descriptionLangKey, options.descriptionLangArgs));
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?",
"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.ViewBot": "VIEW BOT",
"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.Unmute": "Unmute",
"ChatList.Context.Pin": "Pin",

View File

@ -207,7 +207,7 @@ export class AppImManager {
});
rootScope.addEventListener('history_focus', (e) => {
let {peerId, threadId, mid} = e;
let {peerId, threadId, mid, startParam} = e;
if(threadId) threadId = appMessagesIdsManager.generateMessageId(threadId);
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,
lastMsgId: mid,
type: threadId ? 'discussion' : undefined,
threadId
threadId,
startParam
});
});

View File

@ -69,7 +69,7 @@ export type BroadcastEvents = {
'history_delete': {peerId: PeerId, msgs: Set<number>},
'history_forbidden': 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,
'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 {
// margin-left: 0;
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/groupCall";
@import "partials/popups/call";
@import "partials/popups/sponsored";
@import "partials/pages/pages";
@import "partials/pages/authCode";