Browse Source

Sponsored messages

master
morethanwords 2 years ago
parent
commit
c44bba5560
  1. 37
      src/components/chat/bubbles.ts
  2. 56
      src/components/chat/contextMenu.ts
  3. 2
      src/components/chat/selection.ts
  4. 4
      src/components/popups/peer.ts
  5. 42
      src/components/popups/sponsored.ts
  6. 4
      src/lang.ts
  7. 5
      src/lib/appManagers/appImManager.ts
  8. 2
      src/lib/rootScope.ts
  9. 6
      src/scss/partials/_chat.scss
  10. 26
      src/scss/partials/popups/_sponsored.scss
  11. 1
      src/scss/style.scss

37
src/components/chat/bubbles.ts

@ -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);

56
src/components/chat/contextMenu.ts

@ -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 => {
this.buttons.forEach(button => { button.element.classList.toggle('hide', !button.isSponsored);
let good: boolean; });
} else {
//if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) { this.noForwards = !this.appMessagesManager.canForward(this.message);
if(chat.selection.isSelecting && !button.withSelection) {
good = false; this.buttons.forEach(button => {
} else { let good: boolean;
good = contentWrapper || IS_TOUCH_SUPPORTED || true ?
button.verify() : //if((appImManager.chatSelection.isSelecting && !button.withSelection) || (button.withSelection && !appImManager.chatSelection.isSelecting)) {
button.notDirect && button.verify() && button.notDirect(); if(chat.selection.isSelecting && !button.withSelection) {
} good = false;
} else {
button.element.classList.toggle('hide', !good); 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'; 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);

2
src/components/chat/selection.ts

@ -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) => {

4
src/components/popups/peer.ts

@ -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;

42
src/components/popups/sponsored.ts

@ -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();
}
}

4
src/lang.ts

@ -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 can’t 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",

5
src/lib/appManagers/appImManager.ts

@ -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
}); });
}); });

2
src/lib/rootScope.ts

@ -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},

6
src/scss/partials/_chat.scss

@ -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%;
}
}
} }
} }

26
src/scss/partials/popups/_sponsored.scss

@ -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);
}
}
}

1
src/scss/style.scss

@ -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";

Loading…
Cancel
Save