/* 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/media/history_media_web_page.h" #include "layout.h" #include "core/click_handler_types.h" #include "history/history_item_components.h" #include "history/history_item.h" #include "history/history.h" #include "history/view/history_view_element.h" #include "history/view/history_view_cursor_state.h" #include "history/media/history_media_common.h" #include "ui/image/image.h" #include "ui/text_options.h" #include "data/data_session.h" #include "data/data_media_types.h" #include "data/data_web_page.h" #include "data/data_photo.h" #include "data/data_file_origin.h" #include "styles/style_history.h" namespace { using TextState = HistoryView::TextState; } // namespace namespace { constexpr auto kMaxOriginalEntryLines = 8192; int articleThumbWidth(not_null thumb, int height) { auto w = thumb->thumbnail()->width(); auto h = thumb->thumbnail()->height(); return qMax(qMin(height * w / h, height), 1); } int articleThumbHeight(not_null thumb, int width) { return qMax( thumb->thumbnail()->height() * width / thumb->thumbnail()->width(), 1); } std::vector> PrepareCollageMedia( not_null parent, const WebPageCollage &data) { auto result = std::vector>(); result.reserve(data.items.size()); for (const auto item : data.items) { if (const auto document = base::get_if(&item)) { result.push_back(std::make_unique( parent, *document)); } else if (const auto photo = base::get_if(&item)) { result.push_back(std::make_unique( parent, *photo)); } else { return {}; } if (!result.back()->canBeGrouped()) { return {}; } } return result; } } // namespace HistoryWebPage::HistoryWebPage( not_null parent, not_null data) : HistoryMedia(parent) , _data(data) , _title(st::msgMinWidth - st::webPageLeft) , _description(st::msgMinWidth - st::webPageLeft) { history()->owner().registerWebPageView(_data, _parent); } QSize HistoryWebPage::countOptimalSize() { if (_data->pendingTill) { return { 0, 0 }; } const auto versionChanged = (_dataVersion != _data->version); if (versionChanged) { _dataVersion = _data->version; _openl = nullptr; _attach = nullptr; _collage = PrepareCollageMedia(_parent->data(), _data->collage); _title = Text(st::msgMinWidth - st::webPageLeft); _description = Text(st::msgMinWidth - st::webPageLeft); _siteNameWidth = 0; } auto lineHeight = unitedLineHeight(); if (!_openl && !_data->url.isEmpty()) { const auto previewOfHiddenUrl = [&] { const auto simplify = [](const QString &url) { auto result = url.toLower(); if (result.endsWith('/')) { result.chop(1); } const auto prefixes = { qstr("http://"), qstr("https://") }; for (const auto &prefix : prefixes) { if (result.startsWith(prefix)) { result = result.mid(prefix.size()); break; } } return result; }; const auto simplified = simplify(_data->url); const auto full = _parent->data()->originalText(); for (const auto &entity : full.entities) { if (entity.type() != EntityInTextUrl) { continue; } const auto link = full.text.mid( entity.offset(), entity.length()); if (simplify(link) == simplified) { return false; } } return true; }(); _openl = previewOfHiddenUrl ? std::make_shared(_data->url) : std::make_shared(_data->url, true); } // init layout auto title = TextUtilities::SingleLine(_data->title.isEmpty() ? _data->author : _data->title); if (!_collage.empty()) { _asArticle = false; } else if (!_data->document && _data->photo && _data->type != WebPageType::Photo && _data->type != WebPageType::Video) { if (_data->type == WebPageType::Profile) { _asArticle = true; } else if (_data->siteName == qstr("Twitter") || _data->siteName == qstr("Facebook") || _data->type == WebPageType::ArticleWithIV) { _asArticle = false; } else { _asArticle = true; } if (_asArticle && _data->description.text.isEmpty() && title.isEmpty() && _data->siteName.isEmpty()) { _asArticle = false; } } else { _asArticle = false; } // init attach if (!_attach && !_asArticle) { _attach = CreateAttach( _parent, _data->document, _data->photo, _collage); } auto textFloatsAroundInfo = !_asArticle && !_attach && isBubbleBottom(); // init strings if (_description.isEmpty() && !_data->description.text.isEmpty()) { auto text = _data->description; if (textFloatsAroundInfo) { text.text += _parent->skipBlock(); } if (isLogEntryOriginal()) { // Fix layout for small bubbles (narrow media caption edit log entries). _description = Text(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right() - st::webPageLeft); } _description.setMarkedText( st::webPageDescriptionStyle, text, Ui::WebpageTextDescriptionOptions(_data->siteName)); } if (_title.isEmpty() && !title.isEmpty()) { if (textFloatsAroundInfo && _description.isEmpty()) { title += _parent->skipBlock(); } _title.setText( st::webPageTitleStyle, title, Ui::WebpageTextTitleOptions()); } if (!_siteNameWidth && !_data->siteName.isEmpty()) { _siteNameWidth = st::webPageTitleFont->width(_data->siteName); } // init dimensions auto l = st::msgPadding.left() + st::webPageLeft, r = st::msgPadding.right(); auto skipBlockWidth = _parent->skipBlockWidth(); auto maxWidth = skipBlockWidth; auto minHeight = 0; auto siteNameHeight = _data->siteName.isEmpty() ? 0 : lineHeight; auto titleMinHeight = _title.isEmpty() ? 0 : lineHeight; auto descMaxLines = isLogEntryOriginal() ? kMaxOriginalEntryLines : (3 + (siteNameHeight ? 0 : 1) + (titleMinHeight ? 0 : 1)); auto descriptionMinHeight = _description.isEmpty() ? 0 : qMin(_description.minHeight(), descMaxLines * lineHeight); auto articleMinHeight = siteNameHeight + titleMinHeight + descriptionMinHeight; auto articlePhotoMaxWidth = 0; if (_asArticle) { articlePhotoMaxWidth = st::webPagePhotoDelta + qMax(articleThumbWidth(_data->photo, articleMinHeight), lineHeight); } if (_siteNameWidth) { if (_title.isEmpty() && _description.isEmpty()) { accumulate_max(maxWidth, _siteNameWidth + _parent->skipBlockWidth()); } else { accumulate_max(maxWidth, _siteNameWidth + 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) { auto attachAtTop = !_siteNameWidth && _title.isEmpty() && _description.isEmpty(); if (!attachAtTop) minHeight += st::mediaInBubbleSkip; _attach->initDimensions(); auto bubble = _attach->bubbleMargins(); auto maxMediaWidth = _attach->maxWidth() - bubble.left() - bubble.right(); if (isBubbleBottom() && _attach->customInfoLayout()) { maxMediaWidth += skipBlockWidth; } accumulate_max(maxWidth, maxMediaWidth); minHeight += _attach->minHeight() - bubble.top() - bubble.bottom(); if (!_attach->additionalInfoString().isEmpty()) { minHeight += bottomInfoPadding(); } } if (_data->type == WebPageType::Video && _data->duration) { _duration = formatDurationText(_data->duration); _durationWidth = st::msgDateFont->width(_duration); } maxWidth += st::msgPadding.left() + st::webPageLeft + st::msgPadding.right(); auto padding = inBubblePadding(); minHeight += padding.top() + padding.bottom(); if (_asArticle) { minHeight = resizeGetHeight(maxWidth); } return { maxWidth, minHeight }; } QSize HistoryWebPage::countCurrentSize(int newWidth) { if (_data->pendingTill) { return { newWidth, minHeight() }; } auto innerWidth = newWidth - st::msgPadding.left() - st::webPageLeft - st::msgPadding.right(); auto newHeight = 0; auto lineHeight = unitedLineHeight(); auto linesMax = isLogEntryOriginal() ? kMaxOriginalEntryLines : 5; auto siteNameLines = _siteNameWidth ? 1 : 0; auto siteNameHeight = _siteNameWidth ? lineHeight : 0; if (_asArticle) { _pixh = linesMax * lineHeight; do { _pixw = articleThumbWidth(_data->photo, _pixh); auto wleft = innerWidth - st::webPagePhotoDelta - qMax(_pixw, lineHeight); newHeight = siteNameHeight; if (_title.isEmpty()) { _titleLines = 0; } else { if (_title.countHeight(wleft) < 2 * st::webPageTitleFont->height) { _titleLines = 1; } else { _titleLines = 2; } newHeight += _titleLines * lineHeight; } auto descriptionHeight = _description.countHeight(wleft); if (descriptionHeight < (linesMax - siteNameLines - _titleLines) * st::webPageDescriptionFont->height) { // We have height for all the lines. _descriptionLines = -1; newHeight += descriptionHeight; } else { _descriptionLines = (linesMax - siteNameLines - _titleLines); newHeight += _descriptionLines * lineHeight; } if (newHeight >= _pixh) { break; } _pixh -= lineHeight; } while (_pixh > lineHeight); newHeight += bottomInfoPadding(); } else { newHeight = siteNameHeight; if (_title.isEmpty()) { _titleLines = 0; } else { if (_title.countHeight(innerWidth) < 2 * st::webPageTitleFont->height) { _titleLines = 1; } else { _titleLines = 2; } newHeight += _titleLines * lineHeight; } if (_description.isEmpty()) { _descriptionLines = 0; } else { auto descriptionHeight = _description.countHeight(innerWidth); if (descriptionHeight < (linesMax - siteNameLines - _titleLines) * st::webPageDescriptionFont->height) { // We have height for all the lines. _descriptionLines = -1; newHeight += descriptionHeight; } else { _descriptionLines = (linesMax - siteNameLines - _titleLines); newHeight += _descriptionLines * lineHeight; } } if (_attach) { auto attachAtTop = !_siteNameWidth && !_titleLines && !_descriptionLines; if (!attachAtTop) newHeight += st::mediaInBubbleSkip; auto bubble = _attach->bubbleMargins(); _attach->resizeGetHeight(innerWidth + bubble.left() + bubble.right()); newHeight += _attach->height() - bubble.top() - bubble.bottom(); if (!_attach->additionalInfoString().isEmpty()) { newHeight += bottomInfoPadding(); } else if (isBubbleBottom() && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > innerWidth + bubble.left() + bubble.right()) { newHeight += bottomInfoPadding(); } } } auto padding = inBubblePadding(); newHeight += padding.top() + padding.bottom(); return { newWidth, newHeight }; } TextSelection HistoryWebPage::toDescriptionSelection( TextSelection selection) const { return HistoryView::UnshiftItemSelection(selection, _title); } TextSelection HistoryWebPage::fromDescriptionSelection( TextSelection selection) const { return HistoryView::ShiftItemSelection(selection, _title); } void HistoryWebPage::refreshParentId(not_null realParent) { if (_attach) { _attach->refreshParentId(realParent); } } void HistoryWebPage::draw(Painter &p, const QRect &r, TextSelection selection, TimeMs ms) const { if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return; auto paintx = 0, painty = 0, paintw = width(), painth = height(); auto outbg = _parent->hasOutLayout(); bool selected = (selection == FullSelection); auto &barfg = selected ? (outbg ? st::msgOutReplyBarSelColor : st::msgInReplyBarSelColor) : (outbg ? st::msgOutReplyBarColor : st::msgInReplyBarColor); auto &semibold = selected ? (outbg ? st::msgOutServiceFgSelected : st::msgInServiceFgSelected) : (outbg ? st::msgOutServiceFg : st::msgInServiceFg); auto ®ular = selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg); QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); auto padding = inBubblePadding(); auto tshift = padding.top(); auto bshift = padding.bottom(); paintw -= padding.left() + padding.right(); auto attachAdditionalInfoText = _attach ? _attach->additionalInfoString() : QString(); if (_asArticle) { bshift += bottomInfoPadding(); } else if (!attachAdditionalInfoText.isEmpty()) { bshift += bottomInfoPadding(); } else if (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right()) { bshift += bottomInfoPadding(); } QRect bar(rtlrect(st::msgPadding.left(), tshift, st::webPageBar, height() - tshift - bshift, width())); p.fillRect(bar, barfg); auto lineHeight = unitedLineHeight(); if (_asArticle) { const auto contextId = _parent->data()->fullId(); _data->photo->loadThumbnail(contextId); bool full = _data->photo->thumbnail()->loaded(); QPixmap pix; auto pw = qMax(_pixw, lineHeight); auto ph = _pixh; auto pixw = _pixw, pixh = articleThumbHeight(_data->photo, _pixw); const auto maxw = ConvertScale(_data->photo->thumbnail()->width()); const auto maxh = ConvertScale(_data->photo->thumbnail()->height()); if (pixw * ph != pixh * pw) { float64 coef = (pixw * ph > pixh * pw) ? qMin(ph / float64(pixh), maxh / float64(pixh)) : qMin(pw / float64(pixw), maxw / float64(pixw)); pixh = qRound(pixh * coef); pixw = qRound(pixw * coef); } if (full) { pix = _data->photo->thumbnail()->pixSingle(contextId, pixw, pixh, pw, ph, ImageRoundRadius::Small); } else if (_data->photo->thumbnailSmall()->loaded()) { pix = _data->photo->thumbnailSmall()->pixBlurredSingle(contextId, pixw, pixh, pw, ph, ImageRoundRadius::Small); } else if (const auto blurred = _data->photo->thumbnailInline()) { pix = blurred->pixBlurredSingle(contextId, pixw, pixh, pw, ph, ImageRoundRadius::Small); } p.drawPixmapLeft(padding.left() + paintw - pw, tshift, width(), pix); if (selected) { App::roundRect(p, rtlrect(padding.left() + paintw - pw, tshift, pw, _pixh, width()), p.textPalette().selectOverlay, SelectedOverlaySmallCorners); } paintw -= pw + st::webPagePhotoDelta; } if (_siteNameWidth) { p.setFont(st::webPageTitleFont); p.setPen(semibold); p.drawTextLeft(padding.left(), tshift, width(), (paintw >= _siteNameWidth) ? _data->siteName : st::webPageTitleFont->elided(_data->siteName, paintw)); tshift += lineHeight; } if (_titleLines) { p.setPen(outbg ? st::webPageTitleOutFg : st::webPageTitleInFg); auto endskip = 0; if (_title.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } _title.drawLeftElided(p, padding.left(), tshift, paintw, width(), _titleLines, style::al_left, 0, -1, endskip, false, selection); tshift += _titleLines * lineHeight; } if (_descriptionLines) { p.setPen(outbg ? st::webPageDescriptionOutFg : st::webPageDescriptionInFg); auto endskip = 0; if (_description.hasSkipBlock()) { endskip = _parent->skipBlockWidth(); } if (_descriptionLines > 0) { _description.drawLeftElided(p, padding.left(), tshift, paintw, width(), _descriptionLines, style::al_left, 0, -1, endskip, false, toDescriptionSelection(selection)); tshift += _descriptionLines * lineHeight; } else { _description.drawLeft(p, padding.left(), tshift, paintw, width(), style::al_left, 0, -1, toDescriptionSelection(selection)); tshift += _description.countHeight(paintw); } } if (_attach) { auto attachAtTop = !_siteNameWidth && !_titleLines && !_descriptionLines; if (!attachAtTop) tshift += st::mediaInBubbleSkip; auto attachLeft = padding.left() - bubble.left(); auto attachTop = tshift - bubble.top(); if (rtl()) attachLeft = width() - attachLeft - _attach->width(); p.translate(attachLeft, attachTop); auto attachSelection = selected ? FullSelection : TextSelection { 0, 0 }; _attach->draw(p, r.translated(-attachLeft, -attachTop), attachSelection, ms); auto pixwidth = _attach->width(); auto pixheight = _attach->height(); if (_data->type == WebPageType::Video && _collage.empty() && _data->photo && !_data->document) { if (_attach->isReadyForOpen()) { if (_data->siteName == qstr("YouTube")) { 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) { auto dateX = pixwidth - _durationWidth - st::msgDateImgDelta - 2 * st::msgDateImgPadding.x(); auto dateY = pixheight - st::msgDateFont->height - 2 * st::msgDateImgPadding.y() - st::msgDateImgDelta; auto dateW = pixwidth - dateX - st::msgDateImgDelta; auto dateH = pixheight - dateY - st::msgDateImgDelta; App::roundRect(p, dateX, dateY, dateW, dateH, selected ? st::msgDateImgBgSelected : st::msgDateImgBg, selected ? DateSelectedCorners : DateCorners); 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(selected ? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected) : (outbg ? st::msgOutDateFg : st::msgInDateFg)); p.drawTextLeft(st::msgPadding.left(), bar.y() + bar.height() + st::mediaInBubbleSkip, width(), attachAdditionalInfoText); } } } TextState HistoryWebPage::textState(QPoint point, StateRequest request) const { auto result = TextState(_parent); if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) { return result; } auto paintx = 0, painty = 0, paintw = width(), painth = height(); QMargins bubble(_attach ? _attach->bubbleMargins() : QMargins()); auto padding = inBubblePadding(); auto tshift = padding.top(); auto bshift = padding.bottom(); if (_asArticle || (isBubbleBottom() && _attach && _attach->customInfoLayout() && _attach->width() + _parent->skipBlockWidth() > paintw + bubble.left() + bubble.right())) { bshift += bottomInfoPadding(); } paintw -= padding.left() + padding.right(); auto lineHeight = unitedLineHeight(); auto inThumb = false; if (_asArticle) { auto pw = qMax(_pixw, lineHeight); if (rtlrect(padding.left() + paintw - pw, 0, pw, _pixh, width()).contains(point)) { inThumb = true; } paintw -= pw + st::webPagePhotoDelta; } int symbolAdd = 0; if (_siteNameWidth) { tshift += lineHeight; } if (_titleLines) { if (point.y() >= tshift && point.y() < tshift + _titleLines * lineHeight) { Text::StateRequestElided titleRequest = request.forText(); titleRequest.lines = _titleLines; result = TextState(_parent, _title.getStateElidedLeft( point - QPoint(padding.left(), tshift), paintw, width(), titleRequest)); } else if (point.y() >= tshift + _titleLines * lineHeight) { symbolAdd += _title.length(); } tshift += _titleLines * lineHeight; } if (_descriptionLines) { auto descriptionHeight = (_descriptionLines > 0) ? _descriptionLines * lineHeight : _description.countHeight(paintw); if (point.y() >= tshift && point.y() < tshift + descriptionHeight) { if (_descriptionLines > 0) { Text::StateRequestElided descriptionRequest = request.forText(); descriptionRequest.lines = _descriptionLines; result = TextState(_parent, _description.getStateElidedLeft( point - QPoint(padding.left(), tshift), paintw, width(), descriptionRequest)); } else { result = TextState(_parent, _description.getStateLeft( point - QPoint(padding.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) { auto attachAtTop = !_siteNameWidth && !_titleLines && !_descriptionLines; if (!attachAtTop) tshift += st::mediaInBubbleSkip; if (QRect(padding.left(), tshift, paintw, height() - tshift - bshift).contains(point)) { auto attachLeft = padding.left() - bubble.left(); auto attachTop = tshift - bubble.top(); if (rtl()) attachLeft = width() - attachLeft - _attach->width(); result = _attach->textState(point - QPoint(attachLeft, attachTop), request); if (result.link && !_data->document && _data->photo && _collage.empty() && _attach->isReadyForOpen()) { if (_data->type == WebPageType::Profile || _data->type == WebPageType::Video) { result.link = _openl; } else if (_data->type == WebPageType::Photo || _data->siteName == qstr("Twitter") || _data->siteName == qstr("Facebook")) { // leave photo link } else { result.link = _openl; } } } } result.symbol += symbolAdd; return result; } TextSelection HistoryWebPage::adjustSelection(TextSelection selection, TextSelectType type) const { if (!_descriptionLines || selection.to <= _title.length()) { return _title.adjustSelection(selection, type); } auto descriptionSelection = _description.adjustSelection(toDescriptionSelection(selection), type); if (selection.from >= _title.length()) { return fromDescriptionSelection(descriptionSelection); } auto titleSelection = _title.adjustSelection(selection, type); return { titleSelection.from, fromDescriptionSelection(descriptionSelection).to }; } void HistoryWebPage::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { if (_attach) { _attach->clickHandlerActiveChanged(p, active); } } void HistoryWebPage::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) { if (_attach) { _attach->clickHandlerPressedChanged(p, pressed); } } void HistoryWebPage::playAnimation(bool autoplay) { if (_attach) { if (autoplay) { _attach->autoplayAnimation(); } else { _attach->playAnimation(); } } } bool HistoryWebPage::isDisplayed() const { const auto item = _parent->data(); return !_data->pendingTill && !item->Has(); } TextWithEntities HistoryWebPage::selectedText(TextSelection selection) const { auto titleResult = _title.originalTextWithEntities( selection, ExpandLinksAll); auto descriptionResult = _description.originalTextWithEntities( toDescriptionSelection(selection), ExpandLinksAll); if (titleResult.text.isEmpty()) { return descriptionResult; } else if (descriptionResult.text.isEmpty()) { return titleResult; } titleResult.text += '\n'; TextUtilities::Append(titleResult, std::move(descriptionResult)); return titleResult; } QMargins HistoryWebPage::inBubblePadding() const { auto lshift = st::msgPadding.left() + st::webPageLeft; auto rshift = st::msgPadding.right(); auto bshift = isBubbleBottom() ? st::msgPadding.left() : st::mediaInBubbleSkip; auto tshift = isBubbleTop() ? st::msgPadding.left() : st::mediaInBubbleSkip; return QMargins(lshift, tshift, rshift, bshift); } bool HistoryWebPage::isLogEntryOriginal() const { return _parent->data()->isLogEntry() && _parent->media() != this; } int HistoryWebPage::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; } HistoryWebPage::~HistoryWebPage() { history()->owner().unregisterWebPageView(_data, _parent); }