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

56
src/components/chat/contextMenu.ts

@ -19,7 +19,7 @@ import PopupPinMessage from "../popups/unpinMessage"; @@ -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"; @@ -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 { @@ -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 { @@ -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 { @@ -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);
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);
});
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);
});
}
const side: 'left' | 'right' = bubble.classList.contains('is-in') ? 'left' : 'right';
//bubble.parentElement.append(this.element);
@ -361,6 +373,14 @@ export default class ChatContextMenu { @@ -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);

2
src/components/chat/selection.ts

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

4
src/components/popups/peer.ts

@ -27,6 +27,8 @@ export type PopupPeerOptions = PopupOptions & Partial<{ @@ -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 { @@ -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;

42
src/components/popups/sponsored.ts

@ -0,0 +1,42 @@ @@ -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 = { @@ -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 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.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",

5
src/lib/appManagers/appImManager.ts

@ -207,7 +207,7 @@ export class AppImManager { @@ -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 { @@ -215,7 +215,8 @@ export class AppImManager {
peerId,
lastMsgId: mid,
type: threadId ? 'discussion' : undefined,
threadId
threadId,
startParam
});
});

2
src/lib/rootScope.ts

@ -69,7 +69,7 @@ export type BroadcastEvents = { @@ -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},

6
src/scss/partials/_chat.scss

@ -1412,6 +1412,12 @@ $background-transition-total-time: #{$input-transition-time - $background-transi @@ -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%;
}
}
}
}

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

@ -0,0 +1,26 @@ @@ -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; @@ -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";

Loading…
Cancel
Save