1415 lines
41 KiB
C++
1415 lines
41 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 "history/view/media/history_view_web_page.h"
|
|
|
|
#include "core/application.h"
|
|
#include "base/qt/qt_key_modifiers.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "iv/iv_instance.h"
|
|
#include "core/click_handler_types.h"
|
|
#include "core/ui_integration.h"
|
|
#include "data/components/sponsored_messages.h"
|
|
#include "data/stickers/data_custom_emoji.h"
|
|
#include "data/data_file_click_handler.h"
|
|
#include "data/data_photo_media.h"
|
|
#include "data/data_session.h"
|
|
#include "data/data_web_page.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item_components.h"
|
|
#include "history/view/history_view_cursor_state.h"
|
|
#include "history/view/history_view_reply.h"
|
|
#include "history/view/history_view_sponsored_click_handler.h"
|
|
#include "history/view/media/history_view_media_common.h"
|
|
#include "history/view/media/history_view_sticker.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "main/main_session.h"
|
|
#include "menu/menu_sponsored.h"
|
|
#include "ui/chat/chat_style.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/rect.h"
|
|
#include "ui/power_saving.h"
|
|
#include "ui/text/format_values.h"
|
|
#include "ui/text/text_options.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
namespace HistoryView {
|
|
namespace {
|
|
|
|
constexpr auto kMaxOriginalEntryLines = 8192;
|
|
constexpr auto kStickerSetLines = 3;
|
|
|
|
[[nodiscard]] int ArticleThumbWidth(not_null<PhotoData*> thumb, int height) {
|
|
const auto size = thumb->location(Data::PhotoSize::Thumbnail);
|
|
return size.height()
|
|
? std::max(std::min(height * size.width() / size.height(), height), 1)
|
|
: 1;
|
|
}
|
|
|
|
[[nodiscard]] int ArticleThumbHeight(
|
|
not_null<Data::PhotoMedia*> thumb,
|
|
int width) {
|
|
const auto size = thumb->size(Data::PhotoSize::Thumbnail);
|
|
return size.width()
|
|
? std::max(size.height() * width / size.width(), 1)
|
|
: 1;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<std::unique_ptr<Data::Media>> PrepareCollageMedia(
|
|
not_null<HistoryItem*> parent,
|
|
const WebPageCollage &data) {
|
|
auto result = std::vector<std::unique_ptr<Data::Media>>();
|
|
result.reserve(data.items.size());
|
|
const auto spoiler = false;
|
|
for (const auto &item : data.items) {
|
|
if (const auto document = std::get_if<DocumentData*>(&item)) {
|
|
const auto skipPremiumEffect = false;
|
|
result.push_back(std::make_unique<Data::MediaFile>(
|
|
parent,
|
|
*document,
|
|
skipPremiumEffect,
|
|
spoiler,
|
|
/*ttlSeconds = */0));
|
|
} else if (const auto photo = std::get_if<PhotoData*>(&item)) {
|
|
result.push_back(std::make_unique<Data::MediaPhoto>(
|
|
parent,
|
|
*photo,
|
|
spoiler));
|
|
} else {
|
|
return {};
|
|
}
|
|
if (!result.back()->canBeGrouped()) {
|
|
return {};
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QString ExtractHash(
|
|
not_null<WebPageData*> webpage,
|
|
const TextWithEntities &text) {
|
|
const auto simplify = [](const QString &url) {
|
|
auto result = url.split('#')[0].toLower();
|
|
if (result.endsWith('/')) {
|
|
result.chop(1);
|
|
}
|
|
const auto prefixes = { u"http://"_q, u"https://"_q };
|
|
for (const auto &prefix : prefixes) {
|
|
if (result.startsWith(prefix)) {
|
|
result = result.mid(prefix.size());
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
const auto simplified = simplify(webpage->url);
|
|
for (const auto &entity : text.entities) {
|
|
const auto link = (entity.type() == EntityType::Url)
|
|
? text.text.mid(entity.offset(), entity.length())
|
|
: (entity.type() == EntityType::CustomUrl)
|
|
? entity.data()
|
|
: QString();
|
|
if (simplify(link) == simplified) {
|
|
const auto i = link.indexOf('#');
|
|
return (i > 0) ? link.mid(i + 1) : QString();
|
|
}
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
[[nodiscard]] ClickHandlerPtr IvClickHandler(
|
|
not_null<WebPageData*> webpage,
|
|
const TextWithEntities &text) {
|
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
|
const auto my = context.other.value<ClickHandlerContext>();
|
|
if (const auto controller = my.sessionWindow.get()) {
|
|
if (const auto iv = webpage->iv.get()) {
|
|
const auto hash = ExtractHash(webpage, text);
|
|
Core::App().iv().show(controller, iv, hash);
|
|
return;
|
|
} else {
|
|
HiddenUrlClickHandler::Open(webpage->url, context.other);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] ClickHandlerPtr AboutSponsoredClickHandler() {
|
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
|
const auto my = context.other.value<ClickHandlerContext>();
|
|
if (const auto controller = my.sessionWindow.get()) {
|
|
Menu::ShowSponsoredAbout(controller->uiShow());
|
|
}
|
|
});
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities PageToPhrase(not_null<WebPageData*> page) {
|
|
const auto type = page->type;
|
|
const auto text = Ui::Text::Upper(page->iv
|
|
? tr::lng_view_button_iv(tr::now)
|
|
: (type == WebPageType::Theme)
|
|
? tr::lng_view_button_theme(tr::now)
|
|
: (type == WebPageType::Story)
|
|
? tr::lng_view_button_story(tr::now)
|
|
: (type == WebPageType::Message)
|
|
? tr::lng_view_button_message(tr::now)
|
|
: (type == WebPageType::Group)
|
|
? tr::lng_view_button_group(tr::now)
|
|
: (type == WebPageType::WallPaper)
|
|
? tr::lng_view_button_background(tr::now)
|
|
: (type == WebPageType::Channel)
|
|
? tr::lng_view_button_channel(tr::now)
|
|
: (type == WebPageType::GroupWithRequest
|
|
|| type == WebPageType::ChannelWithRequest)
|
|
? tr::lng_view_button_request_join(tr::now)
|
|
: (type == WebPageType::GroupBoost
|
|
|| type == WebPageType::ChannelBoost)
|
|
? tr::lng_view_button_boost(tr::now)
|
|
: (type == WebPageType::Giftcode)
|
|
? tr::lng_view_button_giftcode(tr::now)
|
|
: (type == WebPageType::VoiceChat)
|
|
? tr::lng_view_button_voice_chat(tr::now)
|
|
: (type == WebPageType::Livestream)
|
|
? tr::lng_view_button_voice_chat_channel(tr::now)
|
|
: (type == WebPageType::Bot)
|
|
? tr::lng_view_button_bot(tr::now)
|
|
: (type == WebPageType::User)
|
|
? tr::lng_view_button_user(tr::now)
|
|
: (type == WebPageType::BotApp)
|
|
? tr::lng_view_button_bot_app(tr::now)
|
|
: (page->stickerSet && page->stickerSet->isEmoji)
|
|
? tr::lng_view_button_emojipack(tr::now)
|
|
: (type == WebPageType::StickerSet)
|
|
? tr::lng_view_button_stickerset(tr::now)
|
|
: QString());
|
|
if (page->iv) {
|
|
const auto manager = &page->owner().customEmojiManager();
|
|
const auto &icon = st::historyIvIcon;
|
|
const auto padding = st::historyIvIconPadding;
|
|
return Ui::Text::SingleCustomEmoji(
|
|
manager->registerInternalEmoji(icon, padding)
|
|
).append(text);
|
|
}
|
|
return { text };
|
|
}
|
|
|
|
[[nodiscard]] bool HasButton(not_null<WebPageData*> webpage) {
|
|
const auto type = webpage->type;
|
|
return webpage->iv
|
|
|| (type == WebPageType::Message)
|
|
|| (type == WebPageType::Group)
|
|
|| (type == WebPageType::GroupWithRequest)
|
|
|| (type == WebPageType::GroupBoost)
|
|
|| (type == WebPageType::Channel)
|
|
|| (type == WebPageType::ChannelBoost)
|
|
|| (type == WebPageType::ChannelWithRequest)
|
|
|| (type == WebPageType::Giftcode)
|
|
// || (type == WebPageType::Bot)
|
|
|| (type == WebPageType::User)
|
|
|| (type == WebPageType::VoiceChat)
|
|
|| (type == WebPageType::Livestream)
|
|
|| (type == WebPageType::BotApp)
|
|
|| ((type == WebPageType::Theme)
|
|
&& webpage->document
|
|
&& webpage->document->isTheme())
|
|
|| ((type == WebPageType::Story)
|
|
&& (webpage->photo || webpage->document))
|
|
|| ((type == WebPageType::WallPaper)
|
|
&& webpage->document
|
|
&& webpage->document->isWallPaper())
|
|
|| (type == WebPageType::StickerSet);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
WebPage::WebPage(
|
|
not_null<Element*> parent,
|
|
not_null<WebPageData*> data,
|
|
MediaWebPageFlags flags)
|
|
: Media(parent)
|
|
, _st(st::historyPagePreview)
|
|
, _data(data)
|
|
, _sponsoredData([&]() -> std::optional<SponsoredData> {
|
|
if (!(flags & MediaWebPageFlag::Sponsored)) {
|
|
return std::nullopt;
|
|
}
|
|
const auto &session = _parent->data()->history()->session();
|
|
const auto details = session.sponsoredMessages().lookupDetails(
|
|
_parent->data()->fullId());
|
|
auto result = std::make_optional<SponsoredData>();
|
|
result->buttonText = details.buttonText;
|
|
result->isLinkInternal = details.isLinkInternal;
|
|
result->backgroundEmojiId = details.backgroundEmojiId;
|
|
result->colorIndex = details.colorIndex;
|
|
result->canReport = details.canReport;
|
|
return result;
|
|
}())
|
|
, _siteName(st::msgMinWidth - _st.padding.left() - _st.padding.right())
|
|
, _title(st::msgMinWidth - _st.padding.left() - _st.padding.right())
|
|
, _description(st::msgMinWidth - _st.padding.left() - _st.padding.right())
|
|
, _flags(flags) {
|
|
history()->owner().registerWebPageView(_data, _parent);
|
|
}
|
|
|
|
QSize WebPage::countOptimalSize() {
|
|
if (_data->pendingTill || _data->failed) {
|
|
return { 0, 0 };
|
|
}
|
|
|
|
// Detect _openButtonWidth before counting paddings.
|
|
_openButton = Ui::Text::String();
|
|
if (HasButton(_data)) {
|
|
const auto context = Core::MarkedTextContext{
|
|
.session = &_data->session(),
|
|
.customEmojiRepaint = [] {},
|
|
.customEmojiLoopLimit = 1,
|
|
};
|
|
_openButton.setMarkedText(
|
|
st::semiboldTextStyle,
|
|
PageToPhrase(_data),
|
|
kMarkupTextOptions,
|
|
context);
|
|
} else if (_sponsoredData) {
|
|
if (!_sponsoredData->buttonText.isEmpty()) {
|
|
_openButton.setText(
|
|
st::semiboldTextStyle,
|
|
Ui::Text::Upper(_sponsoredData->buttonText));
|
|
}
|
|
}
|
|
|
|
const auto padding = inBubblePadding() + innerMargin();
|
|
const auto versionChanged = (_dataVersion != _data->version);
|
|
if (versionChanged) {
|
|
_dataVersion = _data->version;
|
|
_openl = nullptr;
|
|
_attach = nullptr;
|
|
_collage = PrepareCollageMedia(_parent->data(), _data->collage);
|
|
const auto min = st::msgMinWidth - rect::m::sum::h(_st.padding);
|
|
_siteName = Ui::Text::String(min);
|
|
_title = Ui::Text::String(min);
|
|
_description = Ui::Text::String(min);
|
|
}
|
|
const auto lineHeight = UnitedLineHeight();
|
|
|
|
if (_data->stickerSet && !_stickerSet) {
|
|
_stickerSet = std::make_unique<StickerSet>();
|
|
for (const auto &sticker : _data->stickerSet->items) {
|
|
if (!sticker->sticker()) {
|
|
continue;
|
|
}
|
|
_stickerSet->views.push_back(
|
|
std::make_unique<Sticker>(_parent, sticker, true));
|
|
}
|
|
const auto side = std::ceil(std::sqrt(_stickerSet->views.size()));
|
|
const auto box = lineHeight * kStickerSetLines;
|
|
const auto single = box / side;
|
|
for (const auto &view : _stickerSet->views) {
|
|
view->setWebpagePart();
|
|
view->initSize(single);
|
|
}
|
|
}
|
|
|
|
if (!_openl && (!_data->url.isEmpty() || _sponsoredData)) {
|
|
const auto original = _parent->data()->originalText();
|
|
const auto previewOfHiddenUrl = [&] {
|
|
if (_data->type == WebPageType::BotApp) {
|
|
// Bot Web Apps always show confirmation on hidden urls.
|
|
//
|
|
// But from the dedicated "Open App" button we don't want
|
|
// to request users confirmation on non-first app opening.
|
|
return false;
|
|
}
|
|
const auto simplify = [](const QString &url) {
|
|
auto result = url.toLower();
|
|
if (result.endsWith('/')) {
|
|
result.chop(1);
|
|
}
|
|
const auto prefixes = { u"http://"_q, u"https://"_q };
|
|
for (const auto &prefix : prefixes) {
|
|
if (result.startsWith(prefix)) {
|
|
result = result.mid(prefix.size());
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
const auto simplified = simplify(_data->url);
|
|
for (const auto &entity : original.entities) {
|
|
if (entity.type() != EntityType::Url) {
|
|
continue;
|
|
}
|
|
const auto link = original.text.mid(
|
|
entity.offset(),
|
|
entity.length());
|
|
if (simplify(link) == simplified) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}();
|
|
_openl = _data->iv
|
|
? IvClickHandler(_data, original)
|
|
: (previewOfHiddenUrl || UrlClickHandler::IsSuspicious(
|
|
_data->url))
|
|
? std::make_shared<HiddenUrlClickHandler>(_data->url)
|
|
: std::make_shared<UrlClickHandler>(_data->url, true);
|
|
if (_data->document
|
|
&& (_data->document->isWallPaper()
|
|
|| _data->document->isTheme())) {
|
|
_openl = std::make_shared<DocumentWrappedClickHandler>(
|
|
std::move(_openl),
|
|
_data->document,
|
|
_parent->data()->fullId());
|
|
}
|
|
if (_sponsoredData) {
|
|
_openl = SponsoredLink(
|
|
_data->url,
|
|
_sponsoredData->isLinkInternal);
|
|
|
|
if (_sponsoredData->canReport) {
|
|
_sponsoredData->hintLink = AboutSponsoredClickHandler();
|
|
}
|
|
}
|
|
}
|
|
|
|
// init layout
|
|
const auto title = TextUtilities::SingleLine(_data->title.isEmpty()
|
|
? _data->author
|
|
: _data->title);
|
|
using Flag = MediaWebPageFlag;
|
|
if (_data->hasLargeMedia && (_flags & Flag::ForceLargeMedia)) {
|
|
_asArticle = 0;
|
|
} else if (_data->hasLargeMedia && (_flags & Flag::ForceSmallMedia)) {
|
|
_asArticle = 1;
|
|
} else {
|
|
_asArticle = _data->computeDefaultSmallMedia();
|
|
}
|
|
|
|
// init attach
|
|
if (!_attach && !_asArticle) {
|
|
_attach = CreateAttach(
|
|
_parent,
|
|
_data->document,
|
|
_data->photo,
|
|
_collage,
|
|
_data->url);
|
|
}
|
|
|
|
// init strings
|
|
if (_description.isEmpty() && !_data->description.text.isEmpty()) {
|
|
const auto &text = _data->description;
|
|
|
|
if (isLogEntryOriginal()) {
|
|
// Fix layout for small bubbles
|
|
// (narrow media caption edit log entries).
|
|
_description = Ui::Text::String(st::minPhotoSize
|
|
- rect::m::sum::h(padding));
|
|
}
|
|
using MarkedTextContext = Core::MarkedTextContext;
|
|
auto context = MarkedTextContext{
|
|
.session = &history()->session(),
|
|
.customEmojiRepaint = [=] { _parent->customEmojiRepaint(); },
|
|
};
|
|
if (_data->siteName == u"Twitter"_q) {
|
|
context.type = MarkedTextContext::HashtagMentionType::Twitter;
|
|
} else if (_data->siteName == u"Instagram"_q) {
|
|
context.type = MarkedTextContext::HashtagMentionType::Instagram;
|
|
}
|
|
_description.setMarkedText(
|
|
st::webPageDescriptionStyle,
|
|
text,
|
|
Ui::WebpageTextDescriptionOptions(),
|
|
context);
|
|
}
|
|
const auto siteName = _data->displayedSiteName();
|
|
if (!siteName.isEmpty()) {
|
|
_siteNameLines = 1;
|
|
_siteName.setMarkedText(
|
|
st::webPageTitleStyle,
|
|
Ui::Text::Link(siteName, _data->url),
|
|
Ui::WebpageTextTitleOptions());
|
|
}
|
|
if (_title.isEmpty() && !title.isEmpty()) {
|
|
if (!_siteNameLines && !_data->url.isEmpty()) {
|
|
_title.setMarkedText(
|
|
st::webPageTitleStyle,
|
|
Ui::Text::Link(title, _data->url),
|
|
Ui::WebpageTextTitleOptions());
|
|
|
|
} else {
|
|
_title.setText(
|
|
st::webPageTitleStyle,
|
|
title,
|
|
Ui::WebpageTextTitleOptions());
|
|
}
|
|
}
|
|
|
|
// init dimensions
|
|
const auto skipBlockWidth = _parent->skipBlockWidth();
|
|
auto maxWidth = skipBlockWidth;
|
|
auto minHeight = 0;
|
|
|
|
const auto siteNameHeight = _siteName.isEmpty() ? 0 : lineHeight;
|
|
const auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight;
|
|
const auto descMaxLines = isLogEntryOriginal()
|
|
? kMaxOriginalEntryLines
|
|
: (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1));
|
|
const auto descriptionMinHeight = _description.isEmpty()
|
|
? 0
|
|
: std::min(_description.minHeight(), descMaxLines * lineHeight);
|
|
const auto articleMinHeight = siteNameHeight
|
|
+ titleMinHeight
|
|
+ descriptionMinHeight;
|
|
const auto articlePhotoMaxWidth = _asArticle
|
|
? st::webPagePhotoDelta
|
|
+ std::max(
|
|
ArticleThumbWidth(_data->photo, articleMinHeight),
|
|
lineHeight)
|
|
: 0;
|
|
|
|
if (!_siteName.isEmpty()) {
|
|
accumulate_max(maxWidth, _siteName.maxWidth() + articlePhotoMaxWidth);
|
|
minHeight += lineHeight;
|
|
}
|
|
if (!_title.isEmpty()) {
|
|
accumulate_max(maxWidth, _title.maxWidth() + articlePhotoMaxWidth);
|
|
minHeight += titleMinHeight;
|
|
}
|
|
if (!_description.isEmpty()) {
|
|
accumulate_max(
|
|
maxWidth,
|
|
_description.maxWidth() + articlePhotoMaxWidth);
|
|
minHeight += descriptionMinHeight;
|
|
}
|
|
if (_attach) {
|
|
const auto attachAtTop = _siteName.isEmpty()
|
|
&& _title.isEmpty()
|
|
&& _description.isEmpty();
|
|
if (!attachAtTop) {
|
|
minHeight += st::mediaInBubbleSkip;
|
|
}
|
|
|
|
_attach->initDimensions();
|
|
const auto bubble = _attach->bubbleMargins();
|
|
auto maxMediaWidth = _attach->maxWidth() - rect::m::sum::h(bubble);
|
|
if (isBubbleBottom() && _attach->customInfoLayout()) {
|
|
maxMediaWidth += skipBlockWidth;
|
|
}
|
|
accumulate_max(maxWidth, maxMediaWidth);
|
|
minHeight += _attach->minHeight() - rect::m::sum::v(bubble);
|
|
}
|
|
if (_data->type == WebPageType::Video && _data->duration) {
|
|
_duration = Ui::FormatDurationText(_data->duration);
|
|
_durationWidth = st::msgDateFont->width(_duration);
|
|
}
|
|
if (!_openButton.isEmpty()) {
|
|
maxWidth += rect::m::sum::h(st::historyPageButtonPadding)
|
|
+ _openButton.maxWidth();
|
|
}
|
|
maxWidth += rect::m::sum::h(padding);
|
|
minHeight += rect::m::sum::v(padding);
|
|
|
|
if (_asArticle) {
|
|
minHeight = resizeGetHeight(maxWidth);
|
|
}
|
|
if (_sponsoredData && _sponsoredData->canReport) {
|
|
_sponsoredData->widthBeforeHint
|
|
= st::webPageTitleStyle.font->width(siteName);
|
|
const auto &font = st::webPageSponsoredHintFont;
|
|
_sponsoredData->hintSize = QSize(
|
|
font->width(tr::lng_sponsored_message_revenue_button(tr::now))
|
|
+ font->height,
|
|
font->height);
|
|
maxWidth += _sponsoredData->hintSize.width();
|
|
}
|
|
return { maxWidth, minHeight };
|
|
}
|
|
|
|
QSize WebPage::countCurrentSize(int newWidth) {
|
|
if (_data->pendingTill || _data->failed) {
|
|
return { newWidth, minHeight() };
|
|
}
|
|
|
|
const auto padding = inBubblePadding() + innerMargin();
|
|
const auto innerWidth = newWidth - rect::m::sum::h(padding);
|
|
auto newHeight = 0;
|
|
|
|
const auto specialRightPix = (_sponsoredData || _stickerSet);
|
|
const auto lineHeight = UnitedLineHeight();
|
|
const auto linesMax = (specialRightPix || isLogEntryOriginal())
|
|
? kMaxOriginalEntryLines
|
|
: 5;
|
|
const auto siteNameHeight = _siteNameLines ? lineHeight : 0;
|
|
const auto twoTitleLines = 2 * st::webPageTitleFont->height;
|
|
const auto descriptionLineHeight = st::webPageDescriptionFont->height;
|
|
if (asArticle() || specialRightPix) {
|
|
constexpr auto kSponsoredUserpicLines = 2;
|
|
_pixh = lineHeight
|
|
* (_stickerSet
|
|
? kStickerSetLines
|
|
: specialRightPix
|
|
? kSponsoredUserpicLines
|
|
: linesMax);
|
|
do {
|
|
_pixw = specialRightPix
|
|
? _pixh
|
|
: ArticleThumbWidth(_data->photo, _pixh);
|
|
const auto wleft = innerWidth
|
|
- st::webPagePhotoDelta
|
|
- std::max(_pixw, lineHeight);
|
|
|
|
newHeight = siteNameHeight;
|
|
|
|
if (_title.isEmpty()) {
|
|
_titleLines = 0;
|
|
} else {
|
|
_titleLines = (_title.countHeight(wleft) < twoTitleLines)
|
|
? 1
|
|
: 2;
|
|
newHeight += _titleLines * lineHeight;
|
|
}
|
|
|
|
const auto descriptionHeight = _description.countHeight(wleft);
|
|
const auto restLines = (linesMax - _siteNameLines - _titleLines);
|
|
if (descriptionHeight < restLines * descriptionLineHeight) {
|
|
// We have height for all the lines.
|
|
_descriptionLines = -1;
|
|
newHeight += descriptionHeight;
|
|
} else {
|
|
_descriptionLines = restLines;
|
|
newHeight += _descriptionLines * lineHeight;
|
|
}
|
|
|
|
if (newHeight >= _pixh) {
|
|
break;
|
|
}
|
|
|
|
_pixh -= lineHeight;
|
|
} while (_pixh > lineHeight);
|
|
} else {
|
|
newHeight = siteNameHeight;
|
|
|
|
if (_title.isEmpty()) {
|
|
_titleLines = 0;
|
|
} else {
|
|
_titleLines = (_title.countHeight(innerWidth) < twoTitleLines)
|
|
? 1
|
|
: 2;
|
|
newHeight += _titleLines * lineHeight;
|
|
}
|
|
|
|
if (_description.isEmpty()) {
|
|
_descriptionLines = 0;
|
|
} else {
|
|
const auto restLines = (linesMax - _siteNameLines - _titleLines);
|
|
const auto descriptionHeight = _description.countHeight(
|
|
innerWidth);
|
|
if (descriptionHeight < restLines * descriptionLineHeight) {
|
|
// We have height for all the lines.
|
|
_descriptionLines = -1;
|
|
newHeight += descriptionHeight;
|
|
} else {
|
|
_descriptionLines = restLines;
|
|
newHeight += _descriptionLines * lineHeight;
|
|
}
|
|
}
|
|
|
|
if (_attach) {
|
|
const auto attachAtTop = !_siteNameLines
|
|
&& !_titleLines
|
|
&& !_descriptionLines;
|
|
if (!attachAtTop) {
|
|
newHeight += st::mediaInBubbleSkip;
|
|
}
|
|
|
|
const auto bubble = _attach->bubbleMargins();
|
|
_attach->resizeGetHeight(innerWidth + rect::m::sum::h(bubble));
|
|
newHeight += _attach->height() - rect::m::sum::v(bubble);
|
|
}
|
|
}
|
|
newHeight += rect::m::sum::v(padding);
|
|
|
|
return { newWidth, newHeight };
|
|
}
|
|
|
|
TextSelection WebPage::toTitleSelection(TextSelection selection) const {
|
|
return UnshiftItemSelection(selection, _siteName);
|
|
}
|
|
|
|
TextSelection WebPage::fromTitleSelection(TextSelection selection) const {
|
|
return ShiftItemSelection(selection, _siteName);
|
|
}
|
|
|
|
TextSelection WebPage::toDescriptionSelection(TextSelection selection) const {
|
|
return UnshiftItemSelection(toTitleSelection(selection), _title);
|
|
}
|
|
|
|
TextSelection WebPage::fromDescriptionSelection(
|
|
TextSelection selection) const {
|
|
return ShiftItemSelection(fromTitleSelection(selection), _title);
|
|
}
|
|
|
|
void WebPage::refreshParentId(not_null<HistoryItem*> realParent) {
|
|
if (_attach) {
|
|
_attach->refreshParentId(realParent);
|
|
}
|
|
}
|
|
|
|
void WebPage::ensurePhotoMediaCreated() const {
|
|
Expects(_data->photo != nullptr);
|
|
|
|
if (_photoMedia) {
|
|
return;
|
|
}
|
|
_photoMedia = _data->photo->createMediaView();
|
|
const auto contextId = _parent->data()->fullId();
|
|
_photoMedia->wanted(Data::PhotoSize::Thumbnail, contextId);
|
|
history()->owner().registerHeavyViewPart(_parent);
|
|
}
|
|
|
|
bool WebPage::hasHeavyPart() const {
|
|
return _photoMedia
|
|
|| (_stickerSet)
|
|
|| (_attach ? _attach->hasHeavyPart() : false);
|
|
}
|
|
|
|
void WebPage::unloadHeavyPart() {
|
|
if (_attach) {
|
|
_attach->unloadHeavyPart();
|
|
}
|
|
_description.unloadPersistentAnimation();
|
|
_photoMedia = nullptr;
|
|
}
|
|
|
|
void WebPage::draw(Painter &p, const PaintContext &context) const {
|
|
if (width() < rect::m::sum::h(st::msgPadding) + 1) {
|
|
return;
|
|
}
|
|
const auto st = context.st;
|
|
const auto sti = context.imageStyle();
|
|
const auto stm = context.messageStyle();
|
|
|
|
const auto bubble = _attach ? _attach->bubbleMargins() : QMargins();
|
|
const auto full = Rect(currentSize());
|
|
const auto outer = full - inBubblePadding();
|
|
const auto inner = outer - innerMargin();
|
|
const auto attachAdditionalInfoText = _attach
|
|
? _attach->additionalInfoString()
|
|
: QString();
|
|
auto tshift = inner.top();
|
|
auto paintw = inner.width();
|
|
|
|
const auto asSponsored = (!!_sponsoredData);
|
|
|
|
const auto selected = context.selected();
|
|
const auto view = parent();
|
|
const auto from = view->data()->contentColorsFrom();
|
|
const auto colorIndex = (asSponsored && _sponsoredData->colorIndex)
|
|
? _sponsoredData->colorIndex
|
|
: from
|
|
? from->colorIndex()
|
|
: view->colorIndex();
|
|
const auto cache = context.outbg
|
|
? stm->replyCache[st->colorPatternIndex(colorIndex)].get()
|
|
: st->coloredReplyCache(selected, colorIndex).get();
|
|
const auto backgroundEmojiId = (asSponsored
|
|
&& _sponsoredData->backgroundEmojiId)
|
|
? _sponsoredData->backgroundEmojiId
|
|
: from
|
|
? from->backgroundEmojiId()
|
|
: DocumentId();
|
|
const auto backgroundEmoji = backgroundEmojiId
|
|
? st->backgroundEmojiData(backgroundEmojiId).get()
|
|
: nullptr;
|
|
const auto backgroundEmojiCache = backgroundEmoji
|
|
? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex(
|
|
selected,
|
|
context.outbg,
|
|
true,
|
|
colorIndex + 1)]
|
|
: nullptr;
|
|
Ui::Text::ValidateQuotePaintCache(*cache, _st);
|
|
Ui::Text::FillQuotePaint(p, outer, *cache, _st);
|
|
if (backgroundEmoji) {
|
|
ValidateBackgroundEmoji(
|
|
backgroundEmojiId,
|
|
backgroundEmoji,
|
|
backgroundEmojiCache,
|
|
cache,
|
|
view);
|
|
if (!backgroundEmojiCache->frames[0].isNull()) {
|
|
FillBackgroundEmoji(p, outer, false, *backgroundEmojiCache);
|
|
}
|
|
}
|
|
|
|
if (_ripple) {
|
|
_ripple->paint(p, outer.x(), outer.y(), width(), &cache->bg);
|
|
if (_ripple->empty()) {
|
|
_ripple = nullptr;
|
|
}
|
|
}
|
|
|
|
auto lineHeight = UnitedLineHeight();
|
|
if (_stickerSet) {
|
|
const auto viewsCount = _stickerSet->views.size();
|
|
const auto box = _pixh;
|
|
const auto topLeft = QPoint(inner.left() + paintw - box, tshift);
|
|
const auto side = std::ceil(std::sqrt(viewsCount));
|
|
const auto single = box / side;
|
|
for (auto i = 0; i < side; i++) {
|
|
for (auto j = 0; j < side; j++) {
|
|
const auto index = i * side + j;
|
|
if (viewsCount <= index) {
|
|
break;
|
|
}
|
|
const auto &view = _stickerSet->views[index];
|
|
const auto size = view->countOptimalSize();
|
|
const auto offsetX = (single - size.width()) / 2.;
|
|
const auto offsetY = (single - size.height()) / 2.;
|
|
const auto x = j * single + offsetX;
|
|
const auto y = i * single + offsetY;
|
|
view->draw(p, context, QRect(QPoint(x, y) + topLeft, size));
|
|
}
|
|
}
|
|
paintw -= box;
|
|
} else if (asArticle()) {
|
|
ensurePhotoMediaCreated();
|
|
|
|
auto pix = QPixmap();
|
|
const auto pw = qMax(_pixw, lineHeight);
|
|
const auto ph = _pixh;
|
|
auto pixw = _pixw;
|
|
auto pixh = ArticleThumbHeight(_photoMedia.get(), _pixw);
|
|
const auto maxsize = _photoMedia->size(Data::PhotoSize::Thumbnail);
|
|
const auto maxw = style::ConvertScale(maxsize.width());
|
|
const auto maxh = style::ConvertScale(maxsize.height());
|
|
if (pixw * ph != pixh * pw) {
|
|
const auto coef = (pixw * ph > pixh * pw)
|
|
? std::min(ph / float64(pixh), maxh / float64(pixh))
|
|
: std::min(pw / float64(pixw), maxw / float64(pixw));
|
|
pixh = std::round(pixh * coef);
|
|
pixw = std::round(pixw * coef);
|
|
}
|
|
const auto size = QSize(pixw, pixh);
|
|
const auto args = Images::PrepareArgs{
|
|
.options = Images::Option::RoundSmall,
|
|
.outer = { pw, ph },
|
|
};
|
|
using namespace Data;
|
|
if (const auto thumbnail = _photoMedia->image(PhotoSize::Thumbnail)) {
|
|
pix = thumbnail->pixSingle(size, args);
|
|
} else if (const auto small = _photoMedia->image(PhotoSize::Small)) {
|
|
pix = small->pixSingle(size, args.blurred());
|
|
} else if (const auto blurred = _photoMedia->thumbnailInline()) {
|
|
pix = blurred->pixSingle(size, args.blurred());
|
|
}
|
|
p.drawPixmapLeft(inner.left() + paintw - pw, tshift, width(), pix);
|
|
if (context.selected()) {
|
|
const auto st = context.st;
|
|
Ui::FillRoundRect(
|
|
p,
|
|
style::rtlrect(
|
|
inner.left() + paintw - pw,
|
|
tshift,
|
|
pw,
|
|
_pixh,
|
|
width()),
|
|
st->msgSelectOverlay(),
|
|
st->msgSelectOverlayCorners(Ui::CachedCornerRadius::Small));
|
|
}
|
|
if (!asSponsored) {
|
|
// Ignore photo width in sponsored messages,
|
|
// as its width only affects the title.
|
|
paintw -= pw + st::webPagePhotoDelta;
|
|
}
|
|
}
|
|
if (_siteNameLines) {
|
|
p.setPen(cache->icon);
|
|
p.setTextPalette(context.outbg
|
|
? stm->semiboldPalette
|
|
: st->coloredTextPalette(selected, colorIndex));
|
|
|
|
const auto endskip = _siteName.hasSkipBlock()
|
|
? _parent->skipBlockWidth()
|
|
: 0;
|
|
_siteName.drawLeftElided(
|
|
p,
|
|
inner.left(),
|
|
tshift,
|
|
paintw,
|
|
width(),
|
|
_siteNameLines,
|
|
style::al_left,
|
|
0,
|
|
-1,
|
|
endskip,
|
|
false,
|
|
context.selection);
|
|
if (asSponsored
|
|
&& _sponsoredData->canReport
|
|
&& (paintw >
|
|
_sponsoredData->widthBeforeHint
|
|
+ _sponsoredData->hintSize.width())) {
|
|
if (_sponsoredData->hintRipple) {
|
|
_sponsoredData->hintRipple->paint(
|
|
p,
|
|
_sponsoredData->lastHintPos.x(),
|
|
_sponsoredData->lastHintPos.y(),
|
|
width(),
|
|
&cache->bg);
|
|
if (_sponsoredData->hintRipple->empty()) {
|
|
_sponsoredData->hintRipple = nullptr;
|
|
}
|
|
}
|
|
|
|
auto color = cache->icon;
|
|
color.setAlphaF(color.alphaF() * 0.15);
|
|
|
|
const auto height = st::webPageSponsoredHintFont->height;
|
|
const auto radius = height / 2;
|
|
|
|
_sponsoredData->lastHintPos = QPointF(
|
|
radius + inner.left() + _sponsoredData->widthBeforeHint,
|
|
tshift + (_siteName.style()->font->height - height) / 2.);
|
|
const auto rect = QRectF(
|
|
_sponsoredData->lastHintPos,
|
|
_sponsoredData->hintSize);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(color);
|
|
p.drawRoundedRect(rect, radius, radius);
|
|
|
|
p.setPen(cache->icon);
|
|
p.setBrush(Qt::NoBrush);
|
|
p.setFont(st::webPageSponsoredHintFont);
|
|
p.drawText(
|
|
rect,
|
|
tr::lng_sponsored_message_revenue_button(tr::now),
|
|
style::al_center);
|
|
}
|
|
tshift += lineHeight;
|
|
|
|
p.setTextPalette(stm->textPalette);
|
|
}
|
|
p.setPen(stm->historyTextFg);
|
|
if (_titleLines) {
|
|
const auto endskip = _title.hasSkipBlock()
|
|
? _parent->skipBlockWidth()
|
|
: 0;
|
|
const auto titleWidth = asSponsored
|
|
? (paintw - _pixh - st::webPagePhotoDelta)
|
|
: paintw;
|
|
_title.drawLeftElided(
|
|
p,
|
|
inner.left(),
|
|
tshift,
|
|
titleWidth,
|
|
width(),
|
|
_titleLines,
|
|
style::al_left,
|
|
0,
|
|
-1,
|
|
endskip,
|
|
false,
|
|
toTitleSelection(context.selection));
|
|
tshift += _titleLines * lineHeight;
|
|
}
|
|
if (_descriptionLines) {
|
|
const auto endskip = _description.hasSkipBlock()
|
|
? _parent->skipBlockWidth()
|
|
: 0;
|
|
_parent->prepareCustomEmojiPaint(p, context, _description);
|
|
_description.draw(p, {
|
|
.position = { inner.left(), tshift },
|
|
.outerWidth = width(),
|
|
.availableWidth = paintw,
|
|
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
|
.now = context.now,
|
|
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
|
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
|
.selection = toDescriptionSelection(context.selection),
|
|
.elisionHeight = ((_descriptionLines > 0)
|
|
? (_descriptionLines * lineHeight)
|
|
: 0),
|
|
.elisionRemoveFromEnd = (_descriptionLines > 0) ? endskip : 0,
|
|
});
|
|
tshift += (_descriptionLines > 0)
|
|
? (_descriptionLines * lineHeight)
|
|
: _description.countHeight(paintw);
|
|
}
|
|
if (_attach) {
|
|
const auto attachAtTop = !_siteNameLines
|
|
&& !_titleLines
|
|
&& !_descriptionLines;
|
|
if (!attachAtTop) {
|
|
tshift += st::mediaInBubbleSkip;
|
|
}
|
|
|
|
const auto attachLeft = rtl()
|
|
? (width() - (inner.left() - bubble.left()) - _attach->width())
|
|
: (inner.left() - bubble.left());
|
|
const auto attachTop = tshift - bubble.top();
|
|
|
|
p.translate(attachLeft, attachTop);
|
|
|
|
_attach->draw(p, context.translated(
|
|
-attachLeft,
|
|
-attachTop
|
|
).withSelection(context.selected()
|
|
? FullSelection
|
|
: TextSelection()));
|
|
const auto pixwidth = _attach->width();
|
|
const auto pixheight = _attach->height();
|
|
|
|
if (_data->type == WebPageType::Video
|
|
&& _collage.empty()
|
|
&& _data->photo
|
|
&& !_data->document) {
|
|
if (_attach->isReadyForOpen()) {
|
|
if (_data->siteName == u"YouTube"_q) {
|
|
st->youtubeIcon().paint(
|
|
p,
|
|
(pixwidth - st::youtubeIcon.width()) / 2,
|
|
(pixheight - st::youtubeIcon.height()) / 2,
|
|
width());
|
|
} else {
|
|
st->videoIcon().paint(
|
|
p,
|
|
(pixwidth - st::videoIcon.width()) / 2,
|
|
(pixheight - st::videoIcon.height()) / 2,
|
|
width());
|
|
}
|
|
}
|
|
if (_durationWidth) {
|
|
const auto dateX = pixwidth
|
|
- _durationWidth
|
|
- st::msgDateImgDelta
|
|
- 2 * st::msgDateImgPadding.x();
|
|
const auto dateY = pixheight
|
|
- st::msgDateFont->height
|
|
- 2 * st::msgDateImgPadding.y()
|
|
- st::msgDateImgDelta;
|
|
const auto dateW = pixwidth - dateX - st::msgDateImgDelta;
|
|
const auto dateH = pixheight - dateY - st::msgDateImgDelta;
|
|
|
|
Ui::FillRoundRect(
|
|
p,
|
|
dateX,
|
|
dateY,
|
|
dateW,
|
|
dateH,
|
|
sti->msgDateImgBg,
|
|
sti->msgDateImgBgCorners);
|
|
|
|
p.setFont(st::msgDateFont);
|
|
p.setPen(st->msgDateImgFg());
|
|
p.drawTextLeft(
|
|
dateX + st::msgDateImgPadding.x(),
|
|
dateY + st::msgDateImgPadding.y(),
|
|
pixwidth,
|
|
_duration);
|
|
}
|
|
}
|
|
|
|
p.translate(-attachLeft, -attachTop);
|
|
|
|
if (!attachAdditionalInfoText.isEmpty()) {
|
|
p.setFont(st::msgDateFont);
|
|
p.setPen(stm->msgDateFg);
|
|
p.drawTextLeft(
|
|
st::msgPadding.left(),
|
|
outer.y() + outer.height() + st::mediaInBubbleSkip,
|
|
width(),
|
|
attachAdditionalInfoText);
|
|
}
|
|
}
|
|
|
|
if (!_openButton.isEmpty()) {
|
|
p.setFont(st::semiboldFont);
|
|
p.setPen(cache->icon);
|
|
const auto end = inner.y() + inner.height() + _st.padding.bottom();
|
|
const auto line = st::historyPageButtonLine;
|
|
auto color = cache->icon;
|
|
color.setAlphaF(color.alphaF() * 0.3);
|
|
p.fillRect(inner.x(), end, inner.width(), line, color);
|
|
_openButton.draw(p, {
|
|
.position = QPoint(
|
|
inner.x() + (inner.width() - _openButton.maxWidth()) / 2,
|
|
end + st::historyPageButtonPadding.top()),
|
|
.availableWidth = paintw,
|
|
.now = context.now,
|
|
});
|
|
}
|
|
}
|
|
|
|
bool WebPage::asArticle() const {
|
|
return _asArticle && (_data->photo != nullptr);
|
|
}
|
|
|
|
TextState WebPage::textState(QPoint point, StateRequest request) const {
|
|
auto result = TextState(_parent);
|
|
|
|
if (width() < rect::m::sum::h(st::msgPadding) + 1) {
|
|
return result;
|
|
}
|
|
const auto bubble = _attach ? _attach->bubbleMargins() : QMargins();
|
|
const auto full = Rect(currentSize());
|
|
auto outer = full - inBubblePadding();
|
|
if (_sponsoredData) {
|
|
outer.translate(0, st::msgDateFont->height);
|
|
}
|
|
const auto inner = outer - innerMargin();
|
|
auto tshift = inner.top();
|
|
auto paintw = inner.width();
|
|
|
|
const auto lineHeight = UnitedLineHeight();
|
|
auto inThumb = false;
|
|
if (asArticle()) {
|
|
const auto pw = std::max(_pixw, lineHeight);
|
|
inThumb = style::rtlrect(
|
|
inner.left() + paintw - pw,
|
|
tshift,
|
|
pw,
|
|
_pixh,
|
|
width()).contains(point);
|
|
paintw -= pw + st::webPagePhotoDelta;
|
|
}
|
|
auto symbolAdd = int(0);
|
|
if (_siteNameLines) {
|
|
if (point.y() >= tshift && point.y() < tshift + lineHeight) {
|
|
auto siteNameRequest = Ui::Text::StateRequestElided(
|
|
request.forText());
|
|
siteNameRequest.lines = _siteNameLines;
|
|
result = TextState(
|
|
_parent,
|
|
_siteName.getStateElidedLeft(
|
|
point - QPoint(inner.left(), tshift),
|
|
paintw,
|
|
width(),
|
|
siteNameRequest));
|
|
} else if (point.y() >= tshift + lineHeight) {
|
|
symbolAdd += _siteName.length();
|
|
}
|
|
tshift += lineHeight;
|
|
}
|
|
if (_titleLines) {
|
|
if (point.y() >= tshift
|
|
&& point.y() < tshift + _titleLines * lineHeight) {
|
|
auto titleRequest = Ui::Text::StateRequestElided(
|
|
request.forText());
|
|
titleRequest.lines = _titleLines;
|
|
result = TextState(
|
|
_parent,
|
|
_title.getStateElidedLeft(
|
|
point - QPoint(inner.left(), tshift),
|
|
paintw,
|
|
width(),
|
|
titleRequest));
|
|
} else if (point.y() >= tshift + _titleLines * lineHeight) {
|
|
symbolAdd += _title.length();
|
|
}
|
|
tshift += _titleLines * lineHeight;
|
|
}
|
|
if (_descriptionLines) {
|
|
const auto descriptionHeight = (_descriptionLines > 0)
|
|
? _descriptionLines * lineHeight
|
|
: _description.countHeight(paintw);
|
|
if (point.y() >= tshift && point.y() < tshift + descriptionHeight) {
|
|
if (_descriptionLines > 0) {
|
|
auto descriptionRequest = Ui::Text::StateRequestElided(
|
|
request.forText());
|
|
descriptionRequest.lines = _descriptionLines;
|
|
result = TextState(
|
|
_parent,
|
|
_description.getStateElidedLeft(
|
|
point - QPoint(inner.left(), tshift),
|
|
paintw,
|
|
width(),
|
|
descriptionRequest));
|
|
} else {
|
|
result = TextState(
|
|
_parent,
|
|
_description.getStateLeft(
|
|
point - QPoint(inner.left(), tshift),
|
|
paintw,
|
|
width(),
|
|
request.forText()));
|
|
}
|
|
} else if (point.y() >= tshift + descriptionHeight) {
|
|
symbolAdd += _description.length();
|
|
}
|
|
tshift += descriptionHeight;
|
|
}
|
|
if (inThumb) {
|
|
result.link = _openl;
|
|
} else if (_attach) {
|
|
const auto attachAtTop = !_siteNameLines
|
|
&& !_titleLines
|
|
&& !_descriptionLines;
|
|
if (!attachAtTop) {
|
|
tshift += st::mediaInBubbleSkip;
|
|
}
|
|
|
|
const auto rect = QRect(
|
|
inner.left(),
|
|
tshift,
|
|
paintw,
|
|
inner.top() + inner.height() - tshift);
|
|
if (rect.contains(point)) {
|
|
const auto attachLeft = rtl()
|
|
? width() - (inner.left() - bubble.left()) - _attach->width()
|
|
: (inner.left() - bubble.left());
|
|
const auto attachTop = tshift - bubble.top();
|
|
result = _attach->textState(
|
|
point - QPoint(attachLeft, attachTop),
|
|
request);
|
|
if (result.cursor == CursorState::Enlarge) {
|
|
result.cursor = CursorState::None;
|
|
} else {
|
|
result.link = replaceAttachLink(result.link);
|
|
}
|
|
}
|
|
}
|
|
if ((!result.link || _sponsoredData) && outer.contains(point)) {
|
|
result.link = _openl;
|
|
}
|
|
if (_sponsoredData && _sponsoredData->canReport) {
|
|
const auto contains = QRectF(
|
|
_sponsoredData->lastHintPos,
|
|
_sponsoredData->hintSize).contains(point
|
|
- QPoint(0, st::msgDateFont->height));
|
|
if (contains) {
|
|
result.link = _sponsoredData->hintLink;
|
|
}
|
|
}
|
|
_lastPoint = point - outer.topLeft();
|
|
|
|
result.symbol += symbolAdd;
|
|
return result;
|
|
}
|
|
|
|
ClickHandlerPtr WebPage::replaceAttachLink(
|
|
const ClickHandlerPtr &link) const {
|
|
if (!_attach->isReadyForOpen()
|
|
|| (_siteName.isEmpty()
|
|
&& _title.isEmpty()
|
|
&& _description.isEmpty())
|
|
|| (_data->document
|
|
&& !_data->document->isWallPaper()
|
|
&& !_data->document->isTheme())
|
|
|| !_data->collage.items.empty()) {
|
|
return link;
|
|
}
|
|
return _openl;
|
|
}
|
|
|
|
TextSelection WebPage::adjustSelection(
|
|
TextSelection selection,
|
|
TextSelectType type) const {
|
|
if ((!_titleLines && !_descriptionLines)
|
|
|| selection.to <= _siteName.length()) {
|
|
return _siteName.adjustSelection(selection, type);
|
|
}
|
|
|
|
const auto titlesLength = _siteName.length() + _title.length();
|
|
const auto titleSelection = _title.adjustSelection(
|
|
toTitleSelection(selection),
|
|
type);
|
|
if ((!_siteNameLines && !_descriptionLines)
|
|
|| (selection.from >= _siteName.length()
|
|
&& selection.to <= titlesLength)) {
|
|
return fromTitleSelection(titleSelection);
|
|
}
|
|
|
|
const auto descriptionSelection = _description.adjustSelection(
|
|
toDescriptionSelection(selection),
|
|
type);
|
|
if ((!_siteNameLines && !_titleLines) || selection.from >= titlesLength) {
|
|
return fromDescriptionSelection(descriptionSelection);
|
|
}
|
|
|
|
return {
|
|
_siteName.adjustSelection(selection, type).from,
|
|
(!_descriptionLines || selection.to <= titlesLength)
|
|
? fromTitleSelection(titleSelection).to
|
|
: fromDescriptionSelection(descriptionSelection).to,
|
|
};
|
|
}
|
|
|
|
uint16 WebPage::fullSelectionLength() const {
|
|
return _siteName.length() + _title.length() + _description.length();
|
|
}
|
|
|
|
void WebPage::clickHandlerActiveChanged(
|
|
const ClickHandlerPtr &p,
|
|
bool active) {
|
|
if (_attach) {
|
|
_attach->clickHandlerActiveChanged(p, active);
|
|
}
|
|
}
|
|
|
|
void WebPage::clickHandlerPressedChanged(
|
|
const ClickHandlerPtr &p,
|
|
bool pressed) {
|
|
if (_sponsoredData && _sponsoredData->hintLink == p) {
|
|
if (pressed) {
|
|
if (!_sponsoredData->hintRipple) {
|
|
const auto owner = &parent()->history()->owner();
|
|
auto ripple = std::make_unique<Ui::RippleAnimation>(
|
|
st::defaultRippleAnimation,
|
|
Ui::RippleAnimation::RoundRectMask(
|
|
_sponsoredData->hintSize,
|
|
_st.radius),
|
|
[=] { owner->requestViewRepaint(parent()); });
|
|
_sponsoredData->hintRipple = std::move(ripple);
|
|
}
|
|
const auto full = Rect(currentSize());
|
|
const auto outer = full - inBubblePadding();
|
|
_sponsoredData->hintRipple->add(_lastPoint
|
|
+ outer.topLeft()
|
|
- _sponsoredData->lastHintPos.toPoint());
|
|
} else if (_sponsoredData->hintRipple) {
|
|
_sponsoredData->hintRipple->lastStop();
|
|
}
|
|
return;
|
|
}
|
|
if (p == _openl) {
|
|
if (pressed) {
|
|
if (!_ripple) {
|
|
const auto full = Rect(currentSize());
|
|
const auto outer = full - inBubblePadding();
|
|
const auto owner = &parent()->history()->owner();
|
|
_ripple = std::make_unique<Ui::RippleAnimation>(
|
|
st::defaultRippleAnimation,
|
|
Ui::RippleAnimation::RoundRectMask(
|
|
outer.size(),
|
|
_st.radius),
|
|
[=] { owner->requestViewRepaint(parent()); });
|
|
}
|
|
_ripple->add(_lastPoint);
|
|
} else if (_ripple) {
|
|
_ripple->lastStop();
|
|
}
|
|
}
|
|
if (_attach) {
|
|
_attach->clickHandlerPressedChanged(p, pressed);
|
|
}
|
|
}
|
|
|
|
bool WebPage::enforceBubbleWidth() const {
|
|
return (_attach != nullptr)
|
|
&& (_data->document != nullptr)
|
|
&& (_data->document->isWallPaper() || _data->document->isTheme());
|
|
}
|
|
|
|
void WebPage::playAnimation(bool autoplay) {
|
|
if (_attach) {
|
|
if (autoplay) {
|
|
_attach->autoplayAnimation();
|
|
} else {
|
|
_attach->playAnimation();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool WebPage::isDisplayed() const {
|
|
return !_data->pendingTill
|
|
&& !_data->failed
|
|
&& !_parent->data()->Has<HistoryMessageLogEntryOriginal>();
|
|
}
|
|
|
|
QString WebPage::additionalInfoString() const {
|
|
return _attach ? _attach->additionalInfoString() : QString();
|
|
}
|
|
|
|
bool WebPage::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {
|
|
return _attach && _attach->toggleSelectionByHandlerClick(p);
|
|
}
|
|
|
|
bool WebPage::allowTextSelectionByHandler(const ClickHandlerPtr &p) const {
|
|
return (p == _openl);
|
|
}
|
|
|
|
bool WebPage::dragItemByHandler(const ClickHandlerPtr &p) const {
|
|
return _attach && _attach->dragItemByHandler(p);
|
|
}
|
|
|
|
TextForMimeData WebPage::selectedText(TextSelection selection) const {
|
|
auto siteNameResult = _siteName.toTextForMimeData(selection);
|
|
auto titleResult = _title.toTextForMimeData(toTitleSelection(selection));
|
|
auto descriptionResult = _description.toTextForMimeData(
|
|
toDescriptionSelection(selection));
|
|
if (titleResult.empty() && descriptionResult.empty()) {
|
|
return siteNameResult;
|
|
} else if (siteNameResult.empty() && descriptionResult.empty()) {
|
|
return titleResult;
|
|
} else if (siteNameResult.empty() && titleResult.empty()) {
|
|
return descriptionResult;
|
|
} else if (siteNameResult.empty()) {
|
|
return titleResult.append('\n').append(std::move(descriptionResult));
|
|
} else if (titleResult.empty()) {
|
|
return siteNameResult
|
|
.append('\n')
|
|
.append(std::move(descriptionResult));
|
|
} else if (descriptionResult.empty()) {
|
|
return siteNameResult.append('\n').append(std::move(titleResult));
|
|
}
|
|
|
|
return siteNameResult
|
|
.append('\n')
|
|
.append(std::move(titleResult))
|
|
.append('\n')
|
|
.append(std::move(descriptionResult));
|
|
}
|
|
|
|
QMargins WebPage::inBubblePadding() const {
|
|
return {
|
|
st::msgPadding.left(),
|
|
isBubbleTop() ? st::msgPadding.left() : 0,
|
|
st::msgPadding.right(),
|
|
isBubbleBottom() ? (st::msgPadding.left() + bottomInfoPadding()) : 0
|
|
};
|
|
}
|
|
|
|
QMargins WebPage::innerMargin() const {
|
|
const auto button = _openButton.isEmpty()
|
|
? 0
|
|
: st::historyPageButtonHeight;
|
|
return _st.padding + QMargins(0, 0, 0, button);
|
|
}
|
|
|
|
bool WebPage::isLogEntryOriginal() const {
|
|
return _parent->data()->isAdminLogEntry() && _parent->media() != this;
|
|
}
|
|
|
|
int WebPage::bottomInfoPadding() const {
|
|
if (!isBubbleBottom()) {
|
|
return 0;
|
|
}
|
|
|
|
auto result = st::msgDateFont->height;
|
|
|
|
// We use padding greater than st::msgPadding.bottom() in the
|
|
// bottom of the bubble so that the left line looks pretty.
|
|
// but if we have bottom skip because of the info display
|
|
// we don't need that additional padding so we replace it
|
|
// back with st::msgPadding.bottom() instead of left().
|
|
result += st::msgPadding.bottom() - st::msgPadding.left();
|
|
return result;
|
|
}
|
|
|
|
WebPage::~WebPage() {
|
|
history()->owner().unregisterWebPageView(_data, _parent);
|
|
if (_photoMedia) {
|
|
history()->owner().keepAlive(base::take(_photoMedia));
|
|
_parent->checkHeavyPart();
|
|
}
|
|
}
|
|
|
|
} // namespace HistoryView
|