Telegram builds a set of features to make users consume content inside the app. This article explains why Telegram needs these features and how they are implemented efficiently.
Content Platform in Messenger
Before we dive into the technical details, we could think about the role of the content platform from the perspective of a messenger. Why is it important although there is no centralized newsfeed inside Telegram?
If we could only pick one metric for an IM to be successful, it’s definitely the reachability of messages. Higher reachability gives users more confidence that messages they send would be viewed by peers reliably, which should be the ultimate cause that keeps them loyal to a messenger. A content platform is proved to be a killer feature to boost reachability as users would use the app more frequently even there are no messages to check, which eventually helps them check new messages faster.
In order to provide a good experience to read content from 3rd party websites, a messenger product needs a mechanism to get structured data. Otherwise, it has to open links within a browser widget, which gives fragmented user experience due to longer page load time and non-native page rendering. The mainstream IM apps employ different ways to improve it. One approach is to ask publishers to provide structured data voluntarily:
- A hosted publishing service. By migrating publishers to use a hosted publishing editor, giant messenger apps like WeChat get structured data from authors directly via its Official Account Platform. It’s one of the largest content distribution services in China that generates billions of page views inside the app per day.
- A sharing SDK for other apps to send links into the messenger and fill the required metadata manually like the title, icon, and description. Again it’s a strategy leveraged by WeChat, it saves the engineering effort of building a generic web crawler that can extract structured data from web pages.
Apparently, the approach only works when your product dominates the market as WeChat is acting in China. It’s not practical to do it in the global market. Telegram has applied smart designs to build its current content system:
- Link Preview that was shipped in April 2015 to show rich preview bubbles for most websites. Telegram crawler is built to extract content from links. It’s similar to Facebook Crawler that reads open graph markups inside HTML content. The crawler runs on Telegram data centers and it doesn’t leak any client information to 3rd party web sites.
- In-App Media Playback was added in the same year to play media from Youtube, Vimeo, and SoundCloud without viewing it in a browser widget. More supported media services have been added later, such as Instagram, Twitch, etc.
- Instant View was introduced in 2016, which is an elegant way to open articles from news services with zero page load time. From the engineering perspective, it’s similar to Facebook Instant Articles that was debuted in 2015.
- Telegraph was also launched along with Instant View. It is a publishing tool for hosting richly formatted articles on Telegram data centers.
- Instant View Platform and Contest were launched in 2017. An online template editor and some generous awards were provided to incentivize users to contribute templates for more websites.
- Instant View 2.0 was shipped in late 2018 with support for RTL, tables, blocks of relevant articles, etc.
To sum up, Link Preview gives a quick impression of links to users via richly formatted bubbles. In-App Media Playback enables users to enjoy core media content in the links without leaving the chat UI. Instant View renders articles natively with zero page load time. Instant View Platform enables users to contribute templates in a crowdsourcing manner to expand support for more websites.
Link Preview
As explained in the previous article about bubbles, ChatMessageItem
can include many types of Media
. One implementation is TelegramMediaWebpage
, which models data of web links.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
final public class TelegramMediaWebpage : Postbox.Media, Equatable {
public var id: Postbox.MediaId? { get }
public let peerIds: [Postbox.PeerId]
public let webpageId: Postbox.MediaId
public let content: SyncCore.TelegramMediaWebpageContent
...
}
public enum TelegramMediaWebpageContent {
case Pending(Int32, String?)
case Loaded(TelegramMediaWebpageLoadedContent)
}
public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let url: String
public let displayUrl: String
public let hash: Int32
public let type: String?
public let websiteName: String?
public let title: String?
public let text: String?
public let embedUrl: String?
public let embedType: String?
public let embedSize: PixelDimensions?
public let duration: Int?
public let author: String?
public let image: TelegramMediaImage?
public let file: TelegramMediaFile?
public let attributes: [TelegramMediaWebpageAttribute]
public let instantPage: InstantPage?
}
ChatMessageWebpageBubbleContentNode
renders link previews in chat bubbles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
private var webPage: TelegramMediaWebpage?
private let contentNode: ChatMessageAttachedContentNode
}
final class ChatMessageAttachedContentNode: ASDisplayNode {
private let lineNode: ASImageNode
private let textNode: TextNode
private let inlineImageNode: TransformImageNode
private var contentImageNode: ChatMessageInteractiveMediaNode?
private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode?
private var contentFileNode: ChatMessageInteractiveFileNode?
private var buttonNode: ChatMessageAttachedContentButtonNode?
private let statusNode: ChatMessageDateAndStatusNode
private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge?
private var linkHighlightingNode: LinkHighlightingNode?
private var message: Message?
private var media: Media?
}
Let’s use a YouTube link to illustrate what would happen to send it and render its preview bubble.
While composing the message, the client detects there is a link inside the input text, it starts an RPC of messages.getWebPagePreview
for preview data. The backend responds with MessageMedia.messageMediaWebPage
, which has the link preview data:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public enum MessageMedia: TypeConstructorDescription {
case messageMediaWebPage(webpage: Api.WebPage)
}
public enum WebPage: TypeConstructorDescription {
case webPage(
flags: Int32, // 127
id: Int64, // 1503448449063263326
url: String, // https://www.youtube.com/watch?v=GEZhD3J89ZE
displayUrl: String, // youtube.com/watch?v=GEZhD3J89ZE
hash: Int32, // 0
type: String?, // video
siteName: String?, // YouTube
title: String?, // WWDC 2020 Special Event Keynote — Apple
description: String?, // Apple WWDC 2020 kicked off with big announcement...
photo: Api.Photo?, // TelegramApi.Api.Photo.photo(flags: 0, id: 6020589086160562979, ...
embedUrl: String?, // https://www.youtube.com/embed/GEZhD3J89ZE
embedType: String?, // iframe
embedWidth: Int32?, // 1280
embedHeight: Int32?, // 720
duration: Int32?, // nil
author: String?, // nil
document: Api.Document?, // nil
cachedPage: Api.Page?, // nil
attributes: [Api.WebPageAttribute]? // nil
)
}
After tapping the send button, an RPC messages.sendMessage
is fired and the client is waiting for a reply from the backend. While waiting, a sent message bubble is added to the chat bubble list. If the client already gets the response of messages.getWebPagePreview
, the bubble is rendered in a beautiful preview bubble. Otherwise, it simply displays a plain text message first and waits for the preview data from the send result in Updates.updates
.
Then after tapping the play button, the function openChatMessageImpl
starts and eventually creates an instance of WebEmbedPlayerNode
to play the YouTube video.
In-App Media Playback
WebEmbedPlayerNode
leverages YouTube IFrame Player API to play videos inside WKWebView
.
- Function
webEmbedType
detects the type of embedded content by tryingextractYoutubeVideoIdAndTimestamp
to extract the YouTube video ID from the URL string. WebEmbedPlayerNode
is initialized withYoutubeEmbedImplementation
.YoutubeEmbedImplementation
loads the HTML templateYoutube.html
from bundled resources, generates the page content with the video ID, and then loads it byWKWebView
usinghttps://youtube.com/
as the base URL.- A bundled JavaScript file
YoutubeUserScrip.js
is also injected to hide the watermark and controls from the embedded YouTube player. YoutubeEmbedImplementation
implements protocol methods to play, pause, and seek the embedded player via JavaScript calls.
Similar approaches are applied to other media services that provide content of long videos or live streamings, such as Vimeo
, Twitch
, and generic
sites that can be embedded as an iframe.
For services like Instagram and TikTok that mostly hosts short videos and photos, Telegram Crawler aggressively caches media content on Telegram data centers and they are served as native videos via SystemVideoContentNode
or NativeVideoContentNode
.
It seems Telegram already maintains a large volume of user interaction data and media content on its own backend, it has the potential to build a competitive content aggregation service IMO if their product team wants to.
Instant View
Let’s use the official Telegram blog on Covid-19 to explain the internals of Instant View. The same RPC messages.getWebPagePreview
is requested while composing the link and this time the response has its value set for the field cachedPage
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public enum WebPage: TypeConstructorDescription {
case webPage(
flags: Int32, // 1311
id: Int64, // 4108701751117811561
url: String, // https://telegram.org/blog/coronavirus
displayUrl: String, // telegram.org/blog/coronavirus
hash: Int32, // 702078769
type: String?, // photo
siteName: String?, // Telegram
title: String?, // Coronavirus News and Verified Channels
description: String?, // Channels are a tool for broadcasting your public messages...
photo: Api.Photo?, // TelegramApi.Api.Photo.photo(flags: 0, id: 5777291004297194213, ...
embedUrl: String?, // nil
embedType: String?, // nil
embedWidth: Int32?, // nil
embedHeight: Int32?, // nil
duration: Int32?, // nil
author: String?, // Telegram
document: Api.Document?, // nil
cachedPage: Api.Page?, // TelegramApi.Api.Page.page(...)
attributes: [Api.WebPageAttribute]? // nil
)
}
public enum Page: TypeConstructorDescription {
case page(
flags: Int32, // 0
url: String, // https://telegram.org/blog/coronavirus
blocks: [Api.PageBlock], // [TelegramApi.Api.PageBlock] 37 values
photos: [Api.Photo], // [TelegramApi.Api.Photo] 5 values
documents: [Api.Document],// [TelegramApi.Api.Document] 2 values
views: Int32? // nil
)
}
// inside blocks
[
PageBlock.pageBlockCover,
PageBlock.pageBlockChannel,
PageBlock.pageBlockTitle,
PageBlock.pageBlockAuthorDate,
PageBlock.pageBlockParagraph,
...
PageBlock.pageBlockRelateArticles
]
Api.Page
models the structured data of a link as a list of PageBlock
. PageBlock
defines 28 types of blocks that are either a display unit or a container of blocks. Having container types gives the power to render complex pages with nested structure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
indirect public enum PageBlock: TypeConstructorDescription {
case pageBlockUnsupported
case pageBlockTitle(text: Api.RichText)
case pageBlockSubtitle(text: Api.RichText)
case pageBlockAuthorDate(author: Api.RichText, publishedDate: Int32)
case pageBlockHeader(text: Api.RichText)
case pageBlockSubheader(text: Api.RichText)
case pageBlockParagraph(text: Api.RichText)
case pageBlockPreformatted(text: Api.RichText, language: String)
case pageBlockFooter(text: Api.RichText)
case pageBlockDivider
case pageBlockAnchor(name: String)
case pageBlockBlockquote(text: Api.RichText, caption: Api.RichText)
case pageBlockPullquote(text: Api.RichText, caption: Api.RichText)
case pageBlockCover(cover: Api.PageBlock) // container
case pageBlockChannel(channel: Api.Chat)
case pageBlockKicker(text: Api.RichText)
case pageBlockTable(flags: Int32, title: Api.RichText, rows: [Api.PageTableRow])
case pageBlockPhoto(flags: Int32, photoId: Int64, caption: Api.PageCaption, url: String?, webpageId: Int64?)
case pageBlockVideo(flags: Int32, videoId: Int64, caption: Api.PageCaption)
case pageBlockAudio(audioId: Int64, caption: Api.PageCaption)
case pageBlockEmbed(flags: Int32, url: String?, html: String?, posterPhotoId: Int64?, w: Int32?, h: Int32?, caption: Api.PageCaption) // container to embed a web view
case pageBlockEmbedPost(url: String, webpageId: Int64, authorPhotoId: Int64, author: String, date: Int32, blocks: [Api.PageBlock], caption: Api.PageCaption) // container
case pageBlockCollage(items: [Api.PageBlock], caption: Api.PageCaption) // container
case pageBlockSlideshow(items: [Api.PageBlock], caption: Api.PageCaption) // container
case pageBlockList(items: [Api.PageListItem]) // container
case pageBlockOrderedList(items: [Api.PageListOrderedItem]) // container
case pageBlockDetails(flags: Int32, blocks: [Api.PageBlock], title: Api.RichText) // container
case pageBlockRelatedArticles(title: Api.RichText, articles: [Api.PageRelatedArticle])
case pageBlockMap(geo: Api.GeoPoint, zoom: Int32, w: Int32, h: Int32, caption: Api.PageCaption)
}
The module InstantPageUI
holds all UI code files of Instant View. InstantPageController
is the core controller and its content node InstantPageControllerNode
manages subnodes and layouts via the function updateLayout
. It enumerates page blocks and creates corresponding types of InstantPageItem
for each block.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private func updateLayout() {
...
let currentLayout = instantPageLayoutForWebPage(webPage, ...)
}
func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, ...) -> InstantPageLayout {
var items: [InstantPageItem] = []
...
for block in pageBlocks {
let blockLayout = layoutInstantPageBlock(webpage: webPage, rtl: rtl, block: block, ...)
let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
items.append(contentsOf: blockItems)
}
...
}
func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: InstantPageBlock, ...) {
...
switch block {
case let .title(text):
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .authorDate(author: author, date: date):
...
...
}
final class InstantPageLayout {
let origin: CGPoint
let contentSize: CGSize
let items: [InstantPageItem]
}
InstantPageController
uses the cached page data to show rendered results instantly. Meanwhile, it also sends an RPC messages.getWebPage
to fetch the latest version via function actualizedWebpage
. So the layout function updateLayout
is usually invoked at least two or more times.
Considering the layout function always runs inside the main thread, it might block the UI if the instant page has huge content blocks. For example, a paragraph with 1MB text that’s extracted from an ebook site would significantly slow down the whole app, while the same amount of text can be easily handled by WKWebView
. Apparently, the current version of Instant View assumes the page is normally short.
Off-topic, WeChat used to distribute articles from official accounts in the form of mobile web sites. In 2018, the client starts to fetch structured data and build HTML content locally, which also caches CSS and JavaScript files in advance. It somehow presents a similar experience of Instant View.
Instant View Platform
Transforming a link from raw HTML to clean and structured blocks is a difficult industrial problem in the field of search engineers and mobile browsers. Telegram invents its own rules language to model the content extraction process. The language is sophisticated and supports variables, functions, extended XPath, etc. You can check out sample templates built for Medium, Telegraph, and Telegram Blog to understand it quickly.
In order to encourage users to contribute and define rules for more web sites, Telegram builds an online IDE and has held two contests with $500k prizes in total. It also gives you the freedom to either make a template publicly to all users or keep it privately on your own website.
Conclusion
Telegram shares how to build a full-featured content service that supports many external publishers and provides a silky smooth reading experience inside a messenger. It has involved sophisticated product thinking and meticulous engineering work, which sets a high standard for its competitors.