Files
AyuGramDesktop/Telegram/SourceFiles/iv/iv_rich_page.cpp
T
2026-06-10 18:37:05 +04:00

1873 lines
57 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/iv_rich_page.h"
#include "base/algorithm.h"
#include "base/flat_map.h"
#include "base/qthelp_url.h"
#include "base/unixtime.h"
#include "base/variant.h"
#include "data/data_document.h"
#include "data/data_peer.h"
#include "data/data_photo.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "data/stickers/data_custom_emoji.h"
#include "iv/markdown/iv_markdown_prepare.h"
#include "iv/markdown/iv_markdown_prepare_serialize.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "ui/text/text_utilities.h"
#include <algorithm>
#include <limits>
#include <QtCore/QSize>
namespace Iv {
namespace {
using Block = RichPage::Block;
using BlockKind = RichPage::BlockKind;
using GroupedMediaIntent = RichPage::GroupedMediaIntent;
using GroupedMediaItem = RichPage::GroupedMediaItem;
using ListItem = RichPage::ListItem;
using ListKind = RichPage::ListKind;
using RelatedArticle = RichPage::RelatedArticle;
using RichText = RichPage::RichText;
using TableAlignment = RichPage::TableAlignment;
using TableCell = RichPage::TableCell;
using TableRow = RichPage::TableRow;
using TableVerticalAlignment = RichPage::TableVerticalAlignment;
using TaskState = RichPage::TaskState;
[[nodiscard]] QString FormulaTexFromSource(QString source) {
source = source.trimmed();
if (source.size() >= 2
&& source.front() == QChar('$')
&& source.back() == QChar('$')) {
source = source.mid(1, source.size() - 2).trimmed();
}
return source;
}
const auto PhotoLargeLevels = u"ydxcwmbsa"_q;
constexpr auto kDefaultMapWidth = 400;
constexpr auto kDefaultMapHeight = 200;
[[nodiscard]] int NonZeroMapWidth(int width) {
return (width > 0) ? width : kDefaultMapWidth;
}
[[nodiscard]] int NonZeroMapHeight(int height) {
return (height > 0) ? height : kDefaultMapHeight;
}
[[nodiscard]] Block MakeBlock(BlockKind kind) {
auto result = Block();
result.kind = kind;
return result;
}
[[nodiscard]] Block MakeHeadingBlock(int level) {
auto result = MakeBlock(BlockKind::Heading);
result.headingLevel = level;
return result;
}
enum class ParseSource {
InstantViewPage,
RichMessage,
};
struct ParseContext {
ParseContext(
not_null<Main::Session*> session,
ParseSource source = ParseSource::InstantViewPage)
: session(session)
, source(source) {
}
not_null<Main::Session*> session;
ParseSource source = ParseSource::InstantViewPage;
base::flat_map<uint64, PhotoData*> photos;
base::flat_map<uint64, DocumentData*> documents;
base::flat_map<uint64, QSize> photoSizes;
struct DocumentInfo {
int width = 0;
int height = 0;
QString fileName;
QString title;
QString performer;
int duration = 0;
bool isVideoFile = false;
bool isAnimation = false;
};
base::flat_map<uint64, DocumentInfo> documentInfos;
bool dropRichTextClickHandlers = false;
};
struct RichMessageMetrics {
int textLength = 0;
int blockCount = 0;
int maxDepth = 0;
int mediaCount = 0;
int maxTableColumns = 0;
int tableColumnMeasurementLimit = 0;
};
using TableOccupancyRow = std::vector<char>;
using TableOccupancyGrid = std::vector<TableOccupancyRow>;
enum class RichTextParseMode {
Normal,
DropClickHandlers,
};
void AccumulateTextLength(
RichMessageMetrics *metrics,
const RichText &text) {
metrics->textLength += int(text.text.text.size());
}
void AccumulateTextLength(
RichMessageMetrics *metrics,
const QString &text) {
metrics->textLength += int(text.size());
}
[[nodiscard]] bool IsMediaKind(BlockKind kind) {
switch (kind) {
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
return true;
default:
return false;
}
}
[[nodiscard]] int NormalizeTableSpan(int span) {
return std::max(span, 1);
}
[[nodiscard]] int ClampTableRowspan(
int rawRowspan,
int row,
int rowCount) {
if ((row < 0) || (row >= rowCount) || (rowCount <= 0)) {
return 0;
}
return std::min(NormalizeTableSpan(rawRowspan), rowCount - row);
}
[[nodiscard]] int ClampTableColspan(
int rawColspan,
int column,
int maxColumns) {
if ((column < 0) || (column >= maxColumns) || (maxColumns <= 0)) {
return 0;
}
return std::min(NormalizeTableSpan(rawColspan), maxColumns - column);
}
[[nodiscard]] bool CanOccupyTableSlots(
const TableOccupancyGrid &occupancy,
int row,
int column,
int rowspan,
int colspan) {
if ((row < 0)
|| (column < 0)
|| (rowspan <= 0)
|| (colspan <= 0)
|| (row >= int(occupancy.size()))) {
return false;
}
const auto rowLimit = std::min(row + rowspan, int(occupancy.size()));
const auto columnLimit = column + colspan;
for (auto currentRow = row; currentRow != rowLimit; ++currentRow) {
const auto &occupied = occupancy[currentRow];
const auto occupiedLimit = std::min(columnLimit, int(occupied.size()));
for (auto currentColumn = column;
currentColumn != occupiedLimit;
++currentColumn) {
if (occupied[currentColumn]) {
return false;
}
}
}
return true;
}
[[nodiscard]] int FirstAvailableTableColumn(
const TableOccupancyGrid &occupancy,
int row,
int rowspan,
int colspan,
int maxColumns) {
if ((row < 0)
|| (row >= int(occupancy.size()))
|| (rowspan <= 0)
|| (colspan <= 0)
|| (maxColumns <= 0)) {
return -1;
}
for (auto column = 0; column != maxColumns; ++column) {
const auto effectiveColspan = ClampTableColspan(
colspan,
column,
maxColumns);
if (effectiveColspan <= 0) {
continue;
}
if (CanOccupyTableSlots(
occupancy,
row,
column,
rowspan,
effectiveColspan)) {
return column;
}
}
return -1;
}
void MarkTableSlots(
TableOccupancyGrid *occupancy,
int row,
int column,
int rowspan,
int colspan) {
if ((row < 0)
|| (column < 0)
|| (rowspan <= 0)
|| (colspan <= 0)
|| (row >= int(occupancy->size()))) {
return;
}
const auto rowLimit = std::min(row + rowspan, int(occupancy->size()));
const auto columnLimit = column + colspan;
for (auto currentRow = row; currentRow != rowLimit; ++currentRow) {
auto &occupied = (*occupancy)[currentRow];
if (columnLimit > int(occupied.size())) {
occupied.resize(columnLimit, false);
}
for (auto currentColumn = column;
currentColumn != columnLimit;
++currentColumn) {
occupied[currentColumn] = true;
}
}
}
[[nodiscard]] int TableColumnCount(const TableOccupancyGrid &occupancy) {
auto result = 0;
for (const auto &row : occupancy) {
result = std::max(result, int(row.size()));
}
return result;
}
[[nodiscard]] int TableColumnMeasurementLimit(int maxTableCols) {
if (maxTableCols <= 0) {
return 1;
}
return (maxTableCols == std::numeric_limits<int>::max())
? maxTableCols
: (maxTableCols + 1);
}
[[nodiscard]] int EffectiveTableColumns(
const std::vector<TableRow> &rows,
int maxColumns) {
if (rows.empty() || maxColumns <= 0) {
return 0;
}
auto occupancy = TableOccupancyGrid(rows.size());
for (auto rowIndex = 0; rowIndex != int(rows.size()); ++rowIndex) {
const auto &row = rows[rowIndex];
for (const auto &cell : row.cells) {
const auto normalizedColspan = NormalizeTableSpan(cell.colspan);
const auto rowspan = ClampTableRowspan(
cell.rowspan,
rowIndex,
int(rows.size()));
if (rowspan <= 0) {
continue;
}
const auto column = FirstAvailableTableColumn(
occupancy,
rowIndex,
rowspan,
normalizedColspan,
maxColumns);
if (column < 0) {
return maxColumns;
}
const auto colspan = ClampTableColspan(
normalizedColspan,
column,
maxColumns);
if (colspan != normalizedColspan) {
return maxColumns;
}
MarkTableSlots(
&occupancy,
rowIndex,
column,
rowspan,
colspan);
}
}
return TableColumnCount(occupancy);
}
void AccumulateBlockMetrics(
RichMessageMetrics *metrics,
const std::vector<Block> &blocks,
int depth);
void AccumulateBlockMetrics(
RichMessageMetrics *metrics,
const Block &block,
int depth) {
++metrics->blockCount;
metrics->maxDepth = std::max(metrics->maxDepth, depth);
AccumulateTextLength(metrics, block.text);
AccumulateTextLength(metrics, block.caption);
AccumulateTextLength(metrics, block.formula);
if (IsMediaKind(block.kind)) {
++metrics->mediaCount;
}
for (const auto &child : block.blocks) {
AccumulateBlockMetrics(metrics, child, depth + 1);
}
for (const auto &item : block.listItems) {
++metrics->blockCount;
AccumulateTextLength(metrics, item.text);
AccumulateBlockMetrics(metrics, item.blocks, depth + 1);
}
for (const auto &item : block.mediaItems) {
if (IsMediaKind(item.kind)) {
++metrics->mediaCount;
}
}
for (const auto &row : block.tableRows) {
++metrics->blockCount;
for (const auto &cell : row.cells) {
AccumulateTextLength(metrics, cell.text);
}
}
metrics->maxTableColumns = std::max(
metrics->maxTableColumns,
EffectiveTableColumns(
block.tableRows,
metrics->tableColumnMeasurementLimit));
}
void AccumulateBlockMetrics(
RichMessageMetrics *metrics,
const std::vector<Block> &blocks,
int depth) {
for (const auto &block : blocks) {
AccumulateBlockMetrics(metrics, block, depth);
}
}
[[nodiscard]] RichMessageMetrics ComputeRichMessageMetrics(
const RichPage &page,
const RichMessageLimits &limits) {
auto result = RichMessageMetrics();
result.tableColumnMeasurementLimit = TableColumnMeasurementLimit(
limits.maxTableCols);
AccumulateBlockMetrics(&result, page.blocks, 1);
return result;
}
[[nodiscard]] QString DateText(TimeId date) {
return langDateTimeFull(base::unixtime::parse(date));
}
[[nodiscard]] bool AddEntity(
TextWithEntities *text,
int from,
EntityType type,
const QString &data = QString()) {
const auto length = text->text.size() - from;
if (length <= 0) {
return true;
}
text->entities.push_back(EntityInText(type, from, length, data));
return true;
}
void AddRichAnchor(
QString *primary,
std::vector<QString> *extra,
QString anchorId) {
if (anchorId.isEmpty()) {
return;
}
if (primary && primary->isEmpty()) {
*primary = std::move(anchorId);
return;
}
if (primary && *primary == anchorId) {
return;
}
if (extra && !ranges::contains(*extra, anchorId)) {
extra->push_back(std::move(anchorId));
}
}
void AppendRich(RichText *to, RichText &&from) {
to->text.append(std::move(from.text));
AddRichAnchor(&to->anchorId, &to->anchorIds, std::move(from.anchorId));
for (auto &anchorId : from.anchorIds) {
AddRichAnchor(&to->anchorId, &to->anchorIds, std::move(anchorId));
}
}
[[nodiscard]] QString RichPageLinkEntityData(
ParseSource source,
const QString &url,
uint64 webpageId) {
return (source == ParseSource::InstantViewPage
&& webpageId
&& !url.isEmpty())
? EncodeRichPageLinkUrl(url, webpageId)
: url;
}
[[nodiscard]] QString MentionNameEntityData(
not_null<Main::Session*> session,
uint64 userId) {
if (userId == 0) {
return QString();
}
const auto loaded = session->data().userLoaded(UserId(userId));
return TextUtilities::MentionNameDataFromFields({
.selfId = session->userId().bare,
.userId = userId,
.accessHash = loaded ? loaded->accessHash() : 0,
});
}
[[nodiscard]] TaskState TaskStateFromFlags(
bool checkbox,
bool checked) {
if (!checkbox) {
return TaskState::None;
}
return checked ? TaskState::Checked : TaskState::Unchecked;
}
[[nodiscard]] uint64 PhotoIdFromMtp(const MTPPhoto &photo) {
return photo.match([](const auto &data) {
return uint64(data.vid().v);
});
}
[[nodiscard]] uint64 DocumentIdFromMtp(const MTPDocument &document) {
return document.match([](const auto &data) {
return uint64(data.vid().v);
});
}
[[nodiscard]] QSize PhotoSizeFromMtp(const MTPPhoto &photo) {
auto result = QSize();
photo.match([](const MTPDphotoEmpty &) {
}, [&](const MTPDphoto &data) {
auto bestLevel = PhotoLargeLevels.size();
const auto assign = [&](const QString &type, int width, int height) {
const auto level = type.isEmpty()
? -1
: PhotoLargeLevels.indexOf(type.front());
if (level >= 0 && level < bestLevel) {
bestLevel = level;
result = QSize(width, height);
}
};
for (const auto &size : data.vsizes().v) {
size.match([](const MTPDphotoSizeEmpty &) {
}, [&](const MTPDphotoSize &row) {
assign(qs(row.vtype()), row.vw().v, row.vh().v);
}, [&](const MTPDphotoCachedSize &row) {
assign(qs(row.vtype()), row.vw().v, row.vh().v);
}, [&](const MTPDphotoStrippedSize &) {
}, [&](const MTPDphotoSizeProgressive &row) {
assign(qs(row.vtype()), row.vw().v, row.vh().v);
}, [&](const MTPDphotoPathSize &) {
});
}
});
return result;
}
[[nodiscard]] ParseContext::DocumentInfo DocumentInfoFromMtp(
const MTPDocument &document) {
auto result = ParseContext::DocumentInfo();
document.match([](const MTPDdocumentEmpty &) {
}, [&](const MTPDdocument &data) {
for (const auto &attribute : data.vattributes().v) {
attribute.match([&](const MTPDdocumentAttributeAudio &row) {
result.duration = row.vduration().v;
result.title = qs(row.vtitle().value_or_empty());
result.performer = qs(row.vperformer().value_or_empty());
}, [&](const MTPDdocumentAttributeFilename &row) {
result.fileName = qs(row.vfile_name());
}, [&](const MTPDdocumentAttributeImageSize &row) {
if (result.width <= 0 || result.height <= 0) {
result.width = row.vw().v;
result.height = row.vh().v;
}
}, [&](const MTPDdocumentAttributeAnimated &) {
result.isAnimation = true;
}, [&](const MTPDdocumentAttributeVideo &row) {
result.isVideoFile = true;
result.width = row.vw().v;
result.height = row.vh().v;
}, [&](const auto &) {
});
}
});
return result;
}
void MergeDocumentInfo(
ParseContext::DocumentInfo *existing,
ParseContext::DocumentInfo info) {
if (!existing) {
return;
}
if (existing->width <= 0 && info.width > 0) {
existing->width = info.width;
}
if (existing->height <= 0 && info.height > 0) {
existing->height = info.height;
}
if (existing->fileName.isEmpty() && !info.fileName.isEmpty()) {
existing->fileName = std::move(info.fileName);
}
if (existing->title.isEmpty() && !info.title.isEmpty()) {
existing->title = std::move(info.title);
}
if (existing->performer.isEmpty() && !info.performer.isEmpty()) {
existing->performer = std::move(info.performer);
}
if (existing->duration <= 0 && info.duration > 0) {
existing->duration = info.duration;
}
if (!existing->isVideoFile && info.isVideoFile) {
existing->isVideoFile = true;
}
if (!existing->isAnimation && info.isAnimation) {
existing->isAnimation = true;
}
}
void RememberPhoto(ParseContext *context, const MTPPhoto &photo) {
const auto id = PhotoIdFromMtp(photo);
if (!id) {
return;
}
context->photos[id] = context->session->data().processPhoto(photo).get();
const auto size = PhotoSizeFromMtp(photo);
const auto existing = context->photoSizes.find(id);
if (!size.isEmpty() || existing == end(context->photoSizes)) {
context->photoSizes[id] = size;
}
}
void RememberDocument(ParseContext *context, const MTPDocument &document) {
const auto id = DocumentIdFromMtp(document);
if (!id) {
return;
}
const auto processed = context->session->data().processDocument(document).get();
context->documents[id] = processed;
auto info = DocumentInfoFromMtp(document);
const auto existing = context->documentInfos.find(id);
if (existing != end(context->documentInfos)) {
MergeDocumentInfo(&existing->second, std::move(info));
} else {
context->documentInfos.emplace(id, std::move(info));
}
}
void RememberWebPageMedia(
ParseContext *context,
const MTPDwebPage &webpage) {
if (const auto photo = webpage.vphoto()) {
RememberPhoto(context, *photo);
}
if (const auto document = webpage.vdocument()) {
RememberDocument(context, *document);
}
}
[[nodiscard]] PhotoData *FindPhoto(
const ParseContext &context,
uint64 id) {
const auto i = context.photos.find(id);
return (i != end(context.photos)) ? i->second : nullptr;
}
[[nodiscard]] DocumentData *FindDocument(
const ParseContext &context,
uint64 id) {
const auto i = context.documents.find(id);
return (i != end(context.documents)) ? i->second : nullptr;
}
[[nodiscard]] QSize FindPhotoSize(
const ParseContext &context,
uint64 id) {
const auto i = context.photoSizes.find(id);
return (i != end(context.photoSizes)) ? i->second : QSize();
}
[[nodiscard]] ParseContext::DocumentInfo FindDocumentInfo(
const ParseContext &context,
uint64 id) {
const auto i = context.documentInfos.find(id);
return (i != end(context.documentInfos))
? i->second
: ParseContext::DocumentInfo();
}
[[nodiscard]] TableAlignment TableCellAlignment(
const MTPDpageTableCell &data) {
if (data.is_align_right()) {
return TableAlignment::Right;
} else if (data.is_align_center()) {
return TableAlignment::Center;
}
return TableAlignment::Left;
}
[[nodiscard]] TableVerticalAlignment TableCellVerticalAlignment(
const MTPDpageTableCell &data) {
if (data.is_valign_bottom()) {
return TableVerticalAlignment::Bottom;
} else if (data.is_valign_middle()) {
return TableVerticalAlignment::Middle;
}
return TableVerticalAlignment::Top;
}
[[nodiscard]] RichText ParseRichText(
const MTPRichText &text,
ParseContext *context,
RichTextParseMode mode = RichTextParseMode::Normal);
[[nodiscard]] RichText ParseCaption(
const MTPPageCaption &caption,
ParseContext *context);
[[nodiscard]] bool AppendRichText(
const MTPRichText &text,
RichText *result,
ParseContext *context,
QString *anchorId,
std::vector<QString> *anchorIds) {
return text.match([&](const MTPDtextEmpty &) {
return true;
}, [&](const MTPDtextPlain &data) {
result->text.append(qs(data.vtext()));
return true;
}, [&](const MTPDtextConcat &data) {
for (const auto &part : data.vtexts().v) {
if (!AppendRichText(part, result, context, anchorId, anchorIds)) {
return false;
}
}
return true;
}, [&](const MTPDtextImage &data) {
const auto replacementText = u"[image]"_q;
if (context->source == ParseSource::RichMessage
|| !data.vdocument_id().v
|| data.vw().v <= 0
|| data.vh().v <= 0) {
result->text.append(replacementText);
return true;
}
const auto entityData = Markdown::SerializeInlineTextObjectEntity({
.kind = Markdown::InlineTextObjectKind::IvImage,
.data = Markdown::InlineTextObjectIvImageData{
.documentId = uint64(data.vdocument_id().v),
.width = data.vw().v,
.height = data.vh().v,
.replacementText = replacementText,
},
});
if (entityData.isEmpty()) {
result->text.append(replacementText);
return true;
}
const auto from = result->text.text.size();
result->text.append(QChar::ObjectReplacementCharacter);
result->text.entities.push_back(EntityInText(
EntityType::CustomEmoji,
from,
1,
entityData));
return true;
}, [&](const MTPDtextMath &data) {
const auto source = FormulaTexFromSource(qs(data.vsource()));
const auto entityData = Markdown::SerializeInlineTextObjectEntity({
.kind = Markdown::InlineTextObjectKind::Formula,
.data = Markdown::InlineTextObjectFormulaData{
.copySource = Markdown::InlineFormulaCopySource(source),
.trimmedTex = source,
},
});
if (entityData.isEmpty()) {
result->text.append(source);
return true;
}
const auto from = result->text.text.size();
result->text.append(QChar::ObjectReplacementCharacter);
result->text.entities.push_back(EntityInText(
EntityType::CustomEmoji,
from,
1,
entityData));
return true;
}, [&](const MTPDtextCustomEmoji &data) {
result->text.append(Ui::Text::SingleCustomEmoji(
::Data::SerializeCustomEmojiId(uint64(data.vdocument_id().v)),
qs(data.valt())));
return true;
}, [&](const MTPDtextBold &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Bold);
}, [&](const MTPDtextItalic &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Italic);
}, [&](const MTPDtextUnderline &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Underline);
}, [&](const MTPDtextStrike &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::StrikeOut);
}, [&](const MTPDtextFixed &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Code);
}, [&](const MTPDtextUrl &data) {
const auto from = result->text.text.size();
if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) {
return false;
}
const auto target = qs(data.vurl());
if (result->text.text.size() == from) {
result->text.append(target);
}
if (context->dropRichTextClickHandlers) {
return true;
}
return AddEntity(
&result->text,
from,
EntityType::CustomUrl,
RichPageLinkEntityData(
context->source,
target,
uint64(data.vwebpage_id().v)));
}, [&](const MTPDtextEmail &data) {
const auto from = result->text.text.size();
if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) {
return false;
}
const auto email = qs(data.vemail());
if (result->text.text.size() == from) {
result->text.append(email);
}
if (context->dropRichTextClickHandlers) {
return true;
}
const auto target = u"mailto:"_q + email;
return AddEntity(&result->text, from, EntityType::CustomUrl, target);
}, [&](const MTPDtextSubscript &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Subscript);
}, [&](const MTPDtextSuperscript &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Superscript);
}, [&](const MTPDtextMarked &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Marked);
}, [&](const MTPDtextSpoiler &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& AddEntity(&result->text, from, EntityType::Spoiler);
}, [&](const MTPDtextMention &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::Mention));
}, [&](const MTPDtextHashtag &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::Hashtag));
}, [&](const MTPDtextBotCommand &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::BotCommand));
}, [&](const MTPDtextCashtag &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::Cashtag));
}, [&](const MTPDtextAutoUrl &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::Url));
}, [&](const MTPDtextAutoEmail &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::Email));
}, [&](const MTPDtextAutoPhone &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::Phone));
}, [&](const MTPDtextBankCard &data) {
const auto from = result->text.text.size();
return AppendRichText(data.vtext(), result, context, anchorId, anchorIds)
&& (context->dropRichTextClickHandlers
|| AddEntity(&result->text, from, EntityType::BankCard));
}, [&](const MTPDtextMentionName &data) {
const auto from = result->text.text.size();
if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) {
return false;
}
if (context->dropRichTextClickHandlers) {
return true;
}
const auto entityData = MentionNameEntityData(
context->session,
uint64(data.vuser_id().v));
return entityData.isEmpty()
? true
: AddEntity(
&result->text,
from,
EntityType::MentionName,
entityData);
}, [&](const MTPDtextDate &data) {
const auto from = result->text.text.size();
if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) {
return false;
}
if (context->dropRichTextClickHandlers) {
return true;
}
auto flags = FormattedDateFlags();
if (data.is_relative()) {
flags |= FormattedDateFlag::Relative;
}
if (data.is_short_time()) {
flags |= FormattedDateFlag::ShortTime;
}
if (data.is_long_time()) {
flags |= FormattedDateFlag::LongTime;
}
if (data.is_short_date()) {
flags |= FormattedDateFlag::ShortDate;
}
if (data.is_long_date()) {
flags |= FormattedDateFlag::LongDate;
}
if (data.is_day_of_week()) {
flags |= FormattedDateFlag::DayOfWeek;
}
return AddEntity(
&result->text,
from,
EntityType::FormattedDate,
SerializeFormattedDateData(data.vdate().v, flags));
}, [&](const MTPDtextPhone &data) {
const auto from = result->text.text.size();
if (!AppendRichText(data.vtext(), result, context, anchorId, anchorIds)) {
return false;
}
const auto phone = qs(data.vphone());
if (result->text.text.size() == from) {
result->text.append(phone);
}
if (context->dropRichTextClickHandlers) {
return true;
}
const auto target = u"tel:"_q + phone;
return AddEntity(&result->text, from, EntityType::CustomUrl, target);
}, [&](const MTPDtextAnchor &data) {
AddRichAnchor(
anchorId,
anchorIds,
Markdown::NormalizeFragmentId(qs(data.vname())));
return AppendRichText(
data.vtext(),
result,
context,
anchorId,
anchorIds);
});
}
[[nodiscard]] RichText ParseRichText(
const MTPRichText &text,
ParseContext *context,
RichTextParseMode mode) {
auto result = RichText();
auto anchorId = QString();
auto anchorIds = std::vector<QString>();
const auto wasDropClickHandlers = context->dropRichTextClickHandlers;
context->dropRichTextClickHandlers
= (mode == RichTextParseMode::DropClickHandlers);
const auto parsed = AppendRichText(
text,
&result,
context,
&anchorId,
&anchorIds);
context->dropRichTextClickHandlers = wasDropClickHandlers;
(void)parsed;
result.anchorId = std::move(anchorId);
result.anchorIds = std::move(anchorIds);
return result;
}
[[nodiscard]] RichText ParseCaption(
const MTPPageCaption &caption,
ParseContext *context) {
auto result = RichText();
auto anchorId = QString();
auto anchorIds = std::vector<QString>();
(void)AppendRichText(
caption.data().vtext(),
&result,
context,
&anchorId,
&anchorIds);
auto credit = RichText();
(void)AppendRichText(
caption.data().vcredit(),
&credit,
context,
&anchorId,
&anchorIds);
if (!credit.text.empty()) {
if (!result.text.empty()) {
result.text.append(QChar('\n'));
}
AppendRich(&result, std::move(credit));
}
result.anchorId = std::move(anchorId);
result.anchorIds = std::move(anchorIds);
return result;
}
void AdoptAnchor(QString *anchorId, RichText *text) {
if (anchorId->isEmpty() && !text->anchorId.isEmpty()) {
*anchorId = std::move(text->anchorId);
text->anchorId.clear();
}
}
void AdoptLeadingParagraphListItemText(ListItem *item) {
if (item->blocks.empty()
|| item->blocks.front().kind != BlockKind::Paragraph) {
return;
}
item->text = std::move(item->blocks.front().text);
item->anchorId = std::move(item->blocks.front().anchorId);
item->blocks.erase(item->blocks.begin());
}
void AppendBlocks(
const QVector<MTPPageBlock> &blocks,
std::vector<Block> *result,
ParseContext *context);
void AppendBlock(
const MTPPageBlock &block,
std::vector<Block> *result,
ParseContext *context) {
block.match([&](const MTPDpageBlockUnsupported &) {
result->push_back(MakeBlock(BlockKind::Unsupported));
}, [&](const MTPDpageBlockTitle &data) {
auto parsed = MakeHeadingBlock(1);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockSubtitle &data) {
auto parsed = MakeHeadingBlock(2);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockAuthorDate &data) {
auto parsed = MakeBlock(BlockKind::AuthorDate);
parsed.text = ParseRichText(data.vauthor(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
parsed.date = data.vpublished_date().v;
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeader &data) {
auto parsed = MakeHeadingBlock(3);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockSubheader &data) {
auto parsed = MakeHeadingBlock(4);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockParagraph &data) {
auto parsed = MakeBlock(BlockKind::Paragraph);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockThinking &data) {
auto parsed = MakeBlock(BlockKind::Thinking);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockPreformatted &data) {
auto parsed = MakeBlock(BlockKind::Code);
parsed.language = qs(data.vlanguage()).trimmed();
parsed.text = ParseRichText(
data.vtext(),
context,
RichTextParseMode::DropClickHandlers);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockFooter &data) {
auto parsed = MakeBlock(BlockKind::Footer);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockDivider &) {
result->push_back(MakeBlock(BlockKind::Divider));
}, [&](const MTPDpageBlockAnchor &data) {
const auto anchorId = Markdown::NormalizeFragmentId(qs(data.vname()));
if (!anchorId.isEmpty()) {
auto parsed = MakeBlock(BlockKind::Anchor);
parsed.anchorId = anchorId;
result->push_back(std::move(parsed));
}
}, [&](const MTPDpageBlockList &data) {
auto parsed = MakeBlock(BlockKind::List);
parsed.listKind = ListKind::Bullet;
parsed.listItems.reserve(data.vitems().v.size());
for (const auto &item : data.vitems().v) {
auto listItem = ListItem();
item.match([&](const MTPDpageListItemText &row) {
listItem.taskState = TaskStateFromFlags(
row.is_checkbox(),
row.is_checked());
listItem.text = ParseRichText(row.vtext(), context);
AdoptAnchor(&listItem.anchorId, &listItem.text);
}, [&](const MTPDpageListItemBlocks &row) {
listItem.taskState = TaskStateFromFlags(
row.is_checkbox(),
row.is_checked());
AppendBlocks(row.vblocks().v, &listItem.blocks, context);
AdoptLeadingParagraphListItemText(&listItem);
});
parsed.listItems.push_back(std::move(listItem));
}
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockBlockquote &data) {
auto parsed = MakeBlock(BlockKind::Quote);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
parsed.caption = ParseRichText(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockBlockquoteBlocks &data) {
auto parsed = MakeBlock(BlockKind::Quote);
AppendBlocks(data.vblocks().v, &parsed.blocks, context);
parsed.caption = ParseRichText(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockPullquote &data) {
auto parsed = MakeBlock(BlockKind::Quote);
parsed.pullquote = true;
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
parsed.caption = ParseRichText(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockPhoto &data) {
const auto photoId = uint64(data.vphoto_id().v);
const auto size = FindPhotoSize(*context, photoId);
auto parsed = MakeBlock(BlockKind::Photo);
if (context->source == ParseSource::InstantViewPage) {
parsed.url = qs(data.vurl().value_or_empty());
}
parsed.width = size.width();
parsed.height = size.height();
parsed.photoId = photoId;
parsed.spoiler = data.is_spoiler();
parsed.photo = FindPhoto(*context, photoId);
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockVideo &data) {
const auto documentId = uint64(data.vvideo_id().v);
const auto info = FindDocumentInfo(*context, documentId);
auto parsed = MakeBlock(BlockKind::Video);
parsed.width = info.width;
parsed.height = info.height;
parsed.documentId = documentId;
parsed.autoplay = data.is_autoplay();
parsed.loop = data.is_loop();
parsed.spoiler = data.is_spoiler();
parsed.document = FindDocument(*context, documentId);
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockCover &data) {
AppendBlock(data.vcover(), result, context);
}, [&](const MTPDpageBlockEmbed &data) {
auto parsed = MakeBlock(BlockKind::Embed);
parsed.url = qs(data.vurl().value_or_empty());
parsed.html = data.vhtml() ? qba(*data.vhtml()) : QByteArray();
parsed.width = data.vw().value_or_empty();
parsed.height = data.vh().value_or_empty();
parsed.fullWidth = data.is_full_width() || (data.vw() && !data.vw()->v);
parsed.fixedHeight = (data.vh() != nullptr);
parsed.allowScrolling = data.is_allow_scrolling();
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockEmbedPost &data) {
auto parsed = MakeBlock(BlockKind::EmbedPost);
parsed.url = qs(data.vurl());
parsed.author = qs(data.vauthor());
parsed.date = data.vdate().v;
parsed.photoId = uint64(data.vauthor_photo_id().v);
parsed.photo = FindPhoto(*context, parsed.photoId);
AppendBlocks(data.vblocks().v, &parsed.blocks, context);
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockCollage &data) {
auto parsed = MakeBlock(BlockKind::GroupedMedia);
parsed.mediaIntent = GroupedMediaIntent::Collage;
parsed.mediaItems.reserve(data.vitems().v.size());
for (const auto &item : data.vitems().v) {
item.match([&](const MTPDpageBlockPhoto &row) {
const auto photoId = uint64(row.vphoto_id().v);
const auto size = FindPhotoSize(*context, photoId);
parsed.mediaItems.push_back({
.kind = BlockKind::Photo,
.photo = FindPhoto(*context, photoId),
.photoId = photoId,
.width = size.width(),
.height = size.height(),
.spoiler = row.is_spoiler(),
});
}, [&](const MTPDpageBlockVideo &row) {
const auto documentId = uint64(row.vvideo_id().v);
const auto info = FindDocumentInfo(*context, documentId);
parsed.mediaItems.push_back({
.kind = BlockKind::Video,
.document = FindDocument(*context, documentId),
.documentId = documentId,
.width = info.width,
.height = info.height,
.autoplay = row.is_autoplay(),
.loop = row.is_loop(),
.spoiler = row.is_spoiler(),
});
}, [](const auto &) {
});
}
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockSlideshow &data) {
auto parsed = MakeBlock(BlockKind::GroupedMedia);
parsed.mediaIntent = GroupedMediaIntent::Slideshow;
parsed.mediaItems.reserve(data.vitems().v.size());
for (const auto &item : data.vitems().v) {
item.match([&](const MTPDpageBlockPhoto &row) {
const auto photoId = uint64(row.vphoto_id().v);
const auto size = FindPhotoSize(*context, photoId);
parsed.mediaItems.push_back({
.kind = BlockKind::Photo,
.photo = FindPhoto(*context, photoId),
.photoId = photoId,
.width = size.width(),
.height = size.height(),
.spoiler = row.is_spoiler(),
});
}, [&](const MTPDpageBlockVideo &row) {
const auto documentId = uint64(row.vvideo_id().v);
const auto info = FindDocumentInfo(*context, documentId);
parsed.mediaItems.push_back({
.kind = BlockKind::Video,
.document = FindDocument(*context, documentId),
.documentId = documentId,
.width = info.width,
.height = info.height,
.autoplay = row.is_autoplay(),
.loop = row.is_loop(),
.spoiler = row.is_spoiler(),
});
}, [](const auto &) {
});
}
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockChannel &data) {
auto parsed = MakeBlock(BlockKind::Channel);
parsed.peer = context->session->data().processChat(data.vchannel()).get();
data.vchannel().match([&](const MTPDchannel &row) {
parsed.channelId = row.vid().v;
parsed.channelTitle = qs(row.vtitle());
if (const auto username = row.vusername()) {
parsed.username = qs(*username);
}
}, [&](const MTPDchannelForbidden &row) {
parsed.channelId = row.vid().v;
parsed.channelTitle = qs(row.vtitle());
}, [&](const MTPDchat &row) {
parsed.channelId = row.vid().v;
parsed.channelTitle = qs(row.vtitle());
}, [&](const MTPDchatForbidden &row) {
parsed.channelId = row.vid().v;
parsed.channelTitle = qs(row.vtitle());
}, [](const auto &) {
});
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockAudio &data) {
const auto documentId = uint64(data.vaudio_id().v);
const auto info = FindDocumentInfo(*context, documentId);
auto parsed = MakeBlock(BlockKind::Audio);
parsed.audioTitle = info.title;
parsed.audioPerformer = info.performer;
parsed.audioFileName = info.fileName;
parsed.documentId = documentId;
parsed.document = FindDocument(*context, documentId);
parsed.audioDuration = info.duration;
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockKicker &data) {
auto parsed = MakeHeadingBlock(5);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeading1 &data) {
auto parsed = MakeHeadingBlock(1);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeading2 &data) {
auto parsed = MakeHeadingBlock(2);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeading3 &data) {
auto parsed = MakeHeadingBlock(3);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeading4 &data) {
auto parsed = MakeHeadingBlock(4);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeading5 &data) {
auto parsed = MakeHeadingBlock(5);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockHeading6 &data) {
auto parsed = MakeHeadingBlock(6);
parsed.text = ParseRichText(data.vtext(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockMath &data) {
auto parsed = MakeBlock(BlockKind::Math);
parsed.formula = qs(data.vsource());
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockTable &data) {
auto parsed = MakeBlock(BlockKind::Table);
parsed.bordered = data.is_bordered();
parsed.striped = data.is_striped();
parsed.text = ParseRichText(data.vtitle(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
parsed.tableRows.reserve(data.vrows().v.size());
for (const auto &row : data.vrows().v) {
row.match([&](const MTPDpageTableRow &rowData) {
auto parsedRow = TableRow();
parsedRow.cells.reserve(rowData.vcells().v.size());
for (const auto &cell : rowData.vcells().v) {
cell.match([&](const MTPDpageTableCell &cellData) {
auto parsedCell = TableCell{
.colspan = cellData.vcolspan().value_or(1),
.rowspan = cellData.vrowspan().value_or(1),
.header = cellData.is_header(),
.alignment = TableCellAlignment(cellData),
.verticalAlignment = TableCellVerticalAlignment(cellData),
};
if (const auto text = cellData.vtext()) {
parsedCell.text = ParseRichText(*text, context);
parsedCell.text.anchorId.clear();
parsedCell.text.anchorIds.clear();
}
parsedRow.cells.push_back(std::move(parsedCell));
});
}
parsed.tableRows.push_back(std::move(parsedRow));
});
}
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockOrderedList &data) {
auto parsed = MakeBlock(BlockKind::List);
parsed.listKind = ListKind::Ordered;
parsed.listItems.reserve(data.vitems().v.size());
const auto step = data.is_reversed() ? -1 : 1;
auto nextNumber = data.vstart().value_or(
data.is_reversed() ? int(data.vitems().v.size()) : 1);
for (const auto &item : data.vitems().v) {
auto listItem = ListItem();
const auto fillNumber = [&](const auto &row) {
if (const auto num = row.vnum()) {
listItem.number = qs(*num);
} else if (const auto value = row.vvalue()) {
listItem.number = QString::number(value->v);
} else {
listItem.number = QString::number(nextNumber);
}
};
item.match([&](const MTPDpageListOrderedItemText &row) {
listItem.taskState = TaskStateFromFlags(
row.is_checkbox(),
row.is_checked());
fillNumber(row);
listItem.text = ParseRichText(row.vtext(), context);
AdoptAnchor(&listItem.anchorId, &listItem.text);
}, [&](const MTPDpageListOrderedItemBlocks &row) {
listItem.taskState = TaskStateFromFlags(
row.is_checkbox(),
row.is_checked());
fillNumber(row);
AppendBlocks(row.vblocks().v, &listItem.blocks, context);
AdoptLeadingParagraphListItemText(&listItem);
});
parsed.listItems.push_back(std::move(listItem));
nextNumber += step;
}
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockDetails &data) {
auto parsed = MakeBlock(BlockKind::Details);
parsed.open = data.is_open();
parsed.text = ParseRichText(data.vtitle(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
AppendBlocks(data.vblocks().v, &parsed.blocks, context);
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockRelatedArticles &data) {
auto parsed = MakeBlock(BlockKind::RelatedArticles);
parsed.text = ParseRichText(data.vtitle(), context);
AdoptAnchor(&parsed.anchorId, &parsed.text);
parsed.relatedArticles.reserve(data.varticles().v.size());
for (const auto &article : data.varticles().v) {
const auto &row = article.data();
parsed.relatedArticles.push_back({
.url = qs(row.vurl()),
.webpageId = uint64(row.vwebpage_id().v),
.photo = FindPhoto(
*context,
uint64(row.vphoto_id().value_or_empty())),
.photoId = uint64(row.vphoto_id().value_or_empty()),
.title = qs(row.vtitle().value_or_empty()).trimmed(),
.description = qs(row.vdescription().value_or_empty()).trimmed(),
.author = qs(row.vauthor().value_or_empty()).trimmed(),
.publishedDate = row.vpublished_date().value_or_empty(),
});
}
result->push_back(std::move(parsed));
}, [&](const MTPDpageBlockMap &data) {
auto parsed = MakeBlock(BlockKind::Map);
parsed.width = NonZeroMapWidth(data.vw().v);
parsed.height = NonZeroMapHeight(data.vh().v);
parsed.zoom = data.vzoom().v;
data.vgeo().match([&](const MTPDgeoPoint &row) {
parsed.latitude = row.vlat().v;
parsed.longitude = row.vlong().v;
parsed.accessHash = row.vaccess_hash().v;
}, [](const auto &) {
});
parsed.caption = ParseCaption(data.vcaption(), context);
AdoptAnchor(&parsed.anchorId, &parsed.caption);
result->push_back(std::move(parsed));
}, [&](const MTPDinputPageBlockMap &) {
result->push_back(MakeBlock(BlockKind::Unsupported));
});
}
void AppendBlocks(
const QVector<MTPPageBlock> &blocks,
std::vector<Block> *result,
ParseContext *context) {
result->reserve(result->size() + blocks.size());
for (const auto &block : blocks) {
AppendBlock(block, result, context);
}
}
void ExpandInlineTextObjects(TextWithEntities *text) {
auto &entities = text->entities;
for (auto i = entities.begin(); i != entities.end();) {
if (i->type() != EntityType::CustomEmoji) {
++i;
continue;
}
const auto object = Markdown::ParseInlineTextObjectEntity(
i->data());
if (!object) {
++i;
continue;
}
const auto replacement = v::match(object->data, [](
const Markdown::InlineTextObjectFormulaData &data) {
return data.trimmedTex;
}, [](const Markdown::InlineTextObjectIvImageData &data) {
return data.replacementText;
});
const auto offset = i->offset();
const auto length = i->length();
const auto delta = int(replacement.size()) - length;
text->text.replace(offset, length, replacement);
i = entities.erase(i);
for (auto &entity : entities) {
if (entity.offset() > offset) {
entity.shiftRight(delta);
} else if (entity.offset() + entity.length() > offset) {
entity.shrinkFromRight(-delta);
}
}
}
}
void AppendSummaryLine(
TextWithEntities *result,
TextWithEntities &&line) {
ExpandInlineTextObjects(&line);
TextUtilities::Trim(line);
if (line.empty()) {
return;
}
if (!result->empty()) {
result->append(QChar('\n'));
}
result->append(std::move(line));
}
void AppendSummaryLine(
TextWithEntities *result,
const TextWithEntities &line,
const QString &prefix = QString()) {
auto prepared = TextWithEntities();
if (!prefix.isEmpty()) {
prepared.append(prefix);
}
prepared.append(line);
AppendSummaryLine(result, std::move(prepared));
}
void AppendSummaryLine(
TextWithEntities *result,
const RichText &line,
const QString &prefix = QString()) {
AppendSummaryLine(result, line.text, prefix);
}
[[nodiscard]] QString MediaSummaryFallback(const Block &block) {
if (block.kind == BlockKind::Photo) {
return tr::lng_in_dlg_photo(tr::now);
} else if (block.kind == BlockKind::Video) {
return tr::lng_in_dlg_video(tr::now);
} else if (block.kind == BlockKind::Audio) {
return tr::lng_in_dlg_audio_file(tr::now);
} else if (block.kind == BlockKind::Map) {
return tr::lng_maps_point(tr::now);
} else if (block.kind == BlockKind::GroupedMedia) {
auto photos = false;
auto videos = false;
for (const auto &item : block.mediaItems) {
if (item.kind == BlockKind::Video) {
videos = true;
} else {
photos = true;
}
}
if (videos && !photos) {
return tr::lng_in_dlg_video(tr::now);
} else if (photos && !videos) {
return tr::lng_in_dlg_photo(tr::now);
}
return tr::lng_in_dlg_album(tr::now);
}
return QString();
}
[[nodiscard]] QString FooterText(const RelatedArticle &article) {
if (article.publishedDate && !article.author.isEmpty()) {
return article.author + u", "_q + DateText(article.publishedDate);
} else if (article.publishedDate) {
return DateText(article.publishedDate);
}
return article.author;
}
void AppendSummaryBlock(TextWithEntities *result, const Block &block);
void AppendSummaryBlocks(
TextWithEntities *result,
const std::vector<Block> &blocks) {
for (const auto &block : blocks) {
AppendSummaryBlock(result, block);
}
}
[[nodiscard]] TextWithEntities FlattenSummaryBlocks(
const std::vector<Block> &blocks) {
auto result = TextWithEntities();
AppendSummaryBlocks(&result, blocks);
TextUtilities::Trim(result);
return result;
}
void AppendSummaryBlock(TextWithEntities *result, const Block &block) {
switch (block.kind) {
case BlockKind::Unsupported:
case BlockKind::Divider:
case BlockKind::Anchor:
case BlockKind::Thinking:
return;
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
AppendSummaryLine(result, block.text);
return;
case BlockKind::AuthorDate: {
auto line = block.text.text;
if (block.date) {
if (!line.empty()) {
line.append(u" - "_q);
}
line.append(DateText(block.date));
}
AppendSummaryLine(result, std::move(line));
return;
}
case BlockKind::List: {
auto ordered = 1;
for (const auto &item : block.listItems) {
auto prefix = QString();
if (item.taskState == TaskState::Unchecked) {
prefix = u"[ ] "_q;
} else if (item.taskState == TaskState::Checked) {
prefix = u"[x] "_q;
} else if (block.listKind == ListKind::Ordered) {
const auto number = item.number.isEmpty()
? QString::number(ordered)
: item.number;
prefix = number + u". "_q;
} else {
prefix = u"- "_q;
}
if (!item.text.text.empty()) {
AppendSummaryLine(result, item.text, prefix);
} else {
auto nested = FlattenSummaryBlocks(item.blocks);
AppendSummaryLine(result, std::move(nested), prefix);
}
++ordered;
}
return;
}
case BlockKind::Quote:
AppendSummaryLine(result, block.text);
AppendSummaryBlocks(result, block.blocks);
AppendSummaryLine(result, block.caption);
return;
case BlockKind::Photo:
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::GroupedMedia:
case BlockKind::Map:
if (!block.caption.text.empty()) {
AppendSummaryLine(result, block.caption);
} else {
AppendSummaryLine(
result,
TextWithEntities::Simple(MediaSummaryFallback(block)));
}
return;
case BlockKind::Embed:
if (!block.caption.text.empty()) {
AppendSummaryLine(result, block.caption);
} else if (!block.url.isEmpty()) {
AppendSummaryLine(result, TextWithEntities::Simple(block.url));
}
return;
case BlockKind::EmbedPost: {
auto line = QString();
if (!block.author.isEmpty()) {
line = block.author;
}
if (block.date) {
if (!line.isEmpty()) {
line += u", "_q;
}
line += DateText(block.date);
}
if (!line.isEmpty()) {
AppendSummaryLine(result, TextWithEntities::Simple(line));
}
AppendSummaryBlocks(result, block.blocks);
AppendSummaryLine(result, block.caption);
return;
}
case BlockKind::Channel:
if (block.peer) {
AppendSummaryLine(
result,
TextWithEntities::Simple(block.peer->name()));
}
return;
case BlockKind::Math:
AppendSummaryLine(result, TextWithEntities::Simple(block.formula));
return;
case BlockKind::Table: {
AppendSummaryLine(result, block.text);
for (const auto &row : block.tableRows) {
auto line = TextWithEntities();
auto first = true;
for (const auto &cell : row.cells) {
auto cellText = TextUtilities::SingleLine(cell.text.text);
TextUtilities::Trim(cellText);
if (cellText.empty()) {
continue;
}
if (!first) {
line.append(u" | "_q);
}
line.append(cellText);
first = false;
}
AppendSummaryLine(result, std::move(line));
}
return;
}
case BlockKind::Details:
AppendSummaryLine(result, block.text);
AppendSummaryBlocks(result, block.blocks);
return;
case BlockKind::RelatedArticles:
AppendSummaryLine(result, block.text);
for (const auto &article : block.relatedArticles) {
if (!article.title.isEmpty()) {
AppendSummaryLine(
result,
TextWithEntities::Simple(article.title));
}
if (!article.description.isEmpty()) {
AppendSummaryLine(
result,
TextWithEntities::Simple(article.description));
}
const auto footer = FooterText(article);
if (!footer.isEmpty()) {
AppendSummaryLine(
result,
TextWithEntities::Simple(footer));
}
}
return;
}
}
std::shared_ptr<const RichPage> ParsePage(
not_null<Main::Session*> session,
const MTPPage &page,
const MTPDwebPage *webpage) {
return page.match([&](const MTPDpage &data) {
auto result = std::make_shared<RichPage>();
auto context = ParseContext(session, ParseSource::InstantViewPage);
result->url = qs(data.vurl());
result->rtl = data.is_rtl();
result->part = data.is_part();
result->views = data.vviews().value_or_empty();
for (const auto &photo : data.vphotos().v) {
RememberPhoto(&context, photo);
}
for (const auto &document : data.vdocuments().v) {
RememberDocument(&context, document);
}
if (webpage) {
RememberWebPageMedia(&context, *webpage);
}
AppendBlocks(data.vblocks().v, &result->blocks, &context);
return std::shared_ptr<const RichPage>(std::move(result));
}, [](const auto &) {
return std::shared_ptr<const RichPage>();
});
}
} // namespace
RichMessageLimits ResolveRichMessageLimits(not_null<Main::Session*> session) {
const auto &config = session->appConfig();
auto result = RichMessageLimits();
result.lengthLimit = config.get<int>(
u"rich_message_length_limit"_q,
result.lengthLimit);
result.maxBlocks = config.get<int>(
u"rich_message_max_blocks"_q,
result.maxBlocks);
result.maxDepth = config.get<int>(
u"rich_message_max_depth"_q,
result.maxDepth);
result.maxMedia = config.get<int>(
u"rich_message_max_media"_q,
result.maxMedia);
result.maxTableCols = config.get<int>(
u"rich_message_max_table_cols"_q,
result.maxTableCols);
return result;
}
std::optional<RichMessageLimitError> ValidateRichMessage(
const RichPage &page,
const RichMessageLimits &limits) {
const auto metrics = ComputeRichMessageMetrics(page, limits);
if (metrics.textLength > limits.lengthLimit) {
return RichMessageLimitError::Length;
} else if (metrics.blockCount > limits.maxBlocks) {
return RichMessageLimitError::Blocks;
} else if (metrics.maxDepth > limits.maxDepth) {
return RichMessageLimitError::Depth;
} else if (metrics.mediaCount > limits.maxMedia) {
return RichMessageLimitError::Media;
} else if (metrics.maxTableColumns > limits.maxTableCols) {
return RichMessageLimitError::TableColumns;
}
return std::nullopt;
}
QString EncodeRichPageLinkUrl(
const QString &url,
uint64 webpageId) {
return u"internal:wrapped?url="_q
+ qthelp::url_encode(url)
+ u"&context=iv&webpage_id="_q
+ QString::number(webpageId);
}
std::optional<RichPageLinkUrl> DecodeRichPageLinkUrl(const QString &data) {
const auto wrappedPrefix = u"internal:wrapped?"_q;
if (!data.startsWith(wrappedPrefix)) {
return std::nullopt;
}
const auto params = qthelp::url_parse_params(data.mid(wrappedPrefix.size()));
if (params.value(u"context"_q) != u"iv"_q) {
return std::nullopt;
}
const auto url = params.value(u"url"_q);
if (url.isEmpty()) {
return std::nullopt;
}
const auto webpageIdValue = params.value(u"webpage_id"_q);
auto webpageId = uint64();
if (!webpageIdValue.isEmpty()) {
auto ok = false;
webpageId = webpageIdValue.toULongLong(&ok);
if (!ok) {
return std::nullopt;
}
}
return RichPageLinkUrl{
.url = url,
.webpageId = webpageId,
};
}
std::shared_ptr<const RichPage> ParseRichPage(
not_null<Main::Session*> session,
const MTPRichMessage &message) {
auto result = std::make_shared<RichPage>();
auto context = ParseContext(session, ParseSource::RichMessage);
const auto &data = message.data();
result->rtl = data.is_rtl();
result->part = data.is_part();
for (const auto &photo : data.vphotos().v) {
RememberPhoto(&context, photo);
}
for (const auto &document : data.vdocuments().v) {
RememberDocument(&context, document);
}
AppendBlocks(data.vblocks().v, &result->blocks, &context);
return result;
}
std::shared_ptr<const RichPage> ParseRichPage(
not_null<Main::Session*> session,
const MTPPage &page) {
return ParsePage(session, page, nullptr);
}
std::shared_ptr<const RichPage> ParseRichPage(
not_null<Main::Session*> session,
const MTPDwebPage &webpage) {
const auto cachedPage = webpage.vcached_page();
if (!cachedPage) {
return std::shared_ptr<const RichPage>();
}
return ParsePage(session, *cachedPage, &webpage);
}
TextWithEntities FlattenRichPageSummary(const RichPage &page) {
auto result = FlattenSummaryBlocks(page.blocks);
TextUtilities::Trim(result);
if (result.empty()) {
result = TextWithEntities::Simple(tr::lng_message_empty(tr::now));
}
return result;
}
TextWithEntities FlattenRichPageSummary(
const std::shared_ptr<const RichPage> &page) {
return page ? FlattenRichPageSummary(*page) : TextWithEntities();
}
} // namespace Iv