440 lines
13 KiB
C++
440 lines
13 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_extended_preview.h"
|
|
|
|
#include "history/history_item.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item_components.h"
|
|
#include "history/view/history_view_element.h"
|
|
#include "history/view/history_view_cursor_state.h"
|
|
#include "history/view/media/history_view_media_common.h"
|
|
#include "media/streaming/media_streaming_utility.h"
|
|
#include "ui/effects/spoiler_mess.h"
|
|
#include "ui/image/image.h"
|
|
#include "ui/image/image_prepare.h"
|
|
#include "ui/chat/chat_style.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/power_saving.h"
|
|
#include "data/data_session.h"
|
|
#include "payments/payments_checkout_process.h"
|
|
#include "window/window_session_controller.h"
|
|
#include "mainwindow.h"
|
|
#include "core/click_handler_types.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
namespace HistoryView {
|
|
namespace {
|
|
|
|
[[nodiscard]] ClickHandlerPtr MakeInvoiceLink(not_null<HistoryItem*> item) {
|
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
|
const auto my = context.other.value<ClickHandlerContext>();
|
|
const auto controller = my.sessionWindow.get();
|
|
Payments::CheckoutProcess::Start(
|
|
item,
|
|
Payments::Mode::Payment,
|
|
(controller
|
|
? crl::guard(
|
|
controller,
|
|
[=](auto) { controller->widget()->activate(); })
|
|
: Fn<void(Payments::CheckoutResult)>()));
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ExtendedPreview::ExtendedPreview(
|
|
not_null<Element*> parent,
|
|
not_null<Data::Invoice*> invoice)
|
|
: Media(parent)
|
|
, _invoice(invoice)
|
|
, _caption(st::minPhotoSize - st::msgPadding.left() - st::msgPadding.right()) {
|
|
const auto item = parent->data();
|
|
_caption = createCaption(item);
|
|
_spoiler.link = MakeInvoiceLink(item);
|
|
resolveButtonText();
|
|
}
|
|
|
|
void ExtendedPreview::resolveButtonText() {
|
|
if (const auto markup = _parent->data()->inlineReplyMarkup()) {
|
|
for (const auto &row : markup->data.rows) {
|
|
for (const auto &button : row) {
|
|
if (button.type == HistoryMessageMarkupButton::Type::Buy) {
|
|
_buttonText.setText(
|
|
st::semiboldTextStyle,
|
|
TextUtilities::SingleLine(button.text));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ExtendedPreview::~ExtendedPreview() {
|
|
if (hasHeavyPart()) {
|
|
unloadHeavyPart();
|
|
_parent->checkHeavyPart();
|
|
}
|
|
}
|
|
|
|
void ExtendedPreview::ensureThumbnailRead() const {
|
|
if (!_inlineThumbnail.isNull() || _imageCacheInvalid) {
|
|
return;
|
|
}
|
|
const auto &bytes = _invoice->extendedPreview.inlineThumbnailBytes;
|
|
if (bytes.isEmpty()) {
|
|
return;
|
|
}
|
|
_inlineThumbnail = Images::FromInlineBytes(bytes);
|
|
if (_inlineThumbnail.isNull()) {
|
|
_imageCacheInvalid = true;
|
|
} else {
|
|
history()->owner().registerHeavyViewPart(_parent);
|
|
}
|
|
}
|
|
|
|
bool ExtendedPreview::hasHeavyPart() const {
|
|
return _spoiler.animation || !_inlineThumbnail.isNull();
|
|
}
|
|
|
|
void ExtendedPreview::unloadHeavyPart() {
|
|
_inlineThumbnail
|
|
= _spoiler.background
|
|
= _spoiler.cornerCache
|
|
= _buttonBackground = QImage();
|
|
_spoiler.animation = nullptr;
|
|
_caption.unloadPersistentAnimation();
|
|
}
|
|
|
|
QSize ExtendedPreview::countOptimalSize() {
|
|
if (_parent->media() != this) {
|
|
_caption = Ui::Text::String();
|
|
} else if (_caption.hasSkipBlock()) {
|
|
_caption.updateSkipBlock(
|
|
_parent->skipBlockWidth(),
|
|
_parent->skipBlockHeight());
|
|
}
|
|
const auto &preview = _invoice->extendedPreview;
|
|
const auto dimensions = preview.dimensions;
|
|
const auto minWidth = std::min(
|
|
std::max({
|
|
_parent->minWidthForMedia(),
|
|
(_parent->hasBubble()
|
|
? st::historyPhotoBubbleMinWidth
|
|
: st::minPhotoSize),
|
|
minWidthForButton(),
|
|
}),
|
|
st::maxMediaSize);
|
|
const auto scaled = CountDesiredMediaSize(dimensions);
|
|
auto maxWidth = qMax(scaled.width(), minWidth);
|
|
auto minHeight = qMax(scaled.height(), st::minPhotoSize);
|
|
if (preview.videoDuration < 0) {
|
|
accumulate_max(maxWidth, scaled.height());
|
|
}
|
|
if (_parent->hasBubble() && !_caption.isEmpty()) {
|
|
maxWidth = qMax(maxWidth, st::msgPadding.left()
|
|
+ _caption.maxWidth()
|
|
+ st::msgPadding.right());
|
|
minHeight += st::mediaCaptionSkip + _caption.minHeight();
|
|
if (isBubbleBottom()) {
|
|
minHeight += st::msgPadding.bottom();
|
|
}
|
|
}
|
|
return { maxWidth, minHeight };
|
|
}
|
|
|
|
QSize ExtendedPreview::countCurrentSize(int newWidth) {
|
|
const auto &preview = _invoice->extendedPreview;
|
|
const auto dimensions = preview.dimensions;
|
|
const auto thumbMaxWidth = std::min(newWidth, st::maxMediaSize);
|
|
const auto minWidth = std::min(
|
|
std::max({
|
|
_parent->minWidthForMedia(),
|
|
(_parent->hasBubble()
|
|
? st::historyPhotoBubbleMinWidth
|
|
: st::minPhotoSize),
|
|
minWidthForButton(),
|
|
}),
|
|
thumbMaxWidth);
|
|
const auto scaled = (preview.videoDuration >= 0)
|
|
? CountMediaSize(
|
|
CountDesiredMediaSize(dimensions),
|
|
newWidth)
|
|
: CountPhotoMediaSize(
|
|
CountDesiredMediaSize(dimensions),
|
|
newWidth,
|
|
maxWidth());
|
|
newWidth = qMax(scaled.width(), minWidth);
|
|
auto newHeight = qMax(scaled.height(), st::minPhotoSize);
|
|
if (_parent->hasBubble() && !_caption.isEmpty()) {
|
|
const auto maxWithCaption = qMin(
|
|
st::msgMaxWidth,
|
|
(st::msgPadding.left()
|
|
+ _caption.maxWidth()
|
|
+ st::msgPadding.right()));
|
|
newWidth = qMin(qMax(newWidth, maxWithCaption), thumbMaxWidth);
|
|
const auto captionw = newWidth
|
|
- st::msgPadding.left()
|
|
- st::msgPadding.right();
|
|
newHeight += st::mediaCaptionSkip + _caption.countHeight(captionw);
|
|
if (isBubbleBottom()) {
|
|
newHeight += st::msgPadding.bottom();
|
|
}
|
|
}
|
|
return { newWidth, newHeight };
|
|
}
|
|
|
|
int ExtendedPreview::minWidthForButton() const {
|
|
return (st::msgBotKbButton.margin + st::msgBotKbButton.padding) * 2
|
|
+ _buttonText.maxWidth();
|
|
}
|
|
|
|
void ExtendedPreview::draw(Painter &p, const PaintContext &context) const {
|
|
if (width() < st::msgPadding.left() + st::msgPadding.right() + 1) return;
|
|
|
|
const auto stm = context.messageStyle();
|
|
auto paintx = 0, painty = 0, paintw = width(), painth = height();
|
|
auto bubble = _parent->hasBubble();
|
|
auto captionw = paintw - st::msgPadding.left() - st::msgPadding.right();
|
|
auto rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
|
|
const auto inWebPage = (_parent->media() != this);
|
|
const auto rounding = inWebPage
|
|
? std::optional<Ui::BubbleRounding>()
|
|
: adjustedBubbleRoundingWithCaption(_caption);
|
|
if (bubble) {
|
|
if (!_caption.isEmpty()) {
|
|
painth -= st::mediaCaptionSkip + _caption.countHeight(captionw);
|
|
if (isBubbleBottom()) {
|
|
painth -= st::msgPadding.bottom();
|
|
}
|
|
rthumb = style::rtlrect(paintx, painty, paintw, painth, width());
|
|
}
|
|
} else {
|
|
Assert(rounding.has_value());
|
|
fillImageShadow(p, rthumb, *rounding, context);
|
|
}
|
|
validateImageCache(rthumb.size(), rounding);
|
|
p.drawImage(rthumb.topLeft(), _spoiler.background);
|
|
fillImageSpoiler(p, &_spoiler, rthumb, context);
|
|
paintButton(p, rthumb, context);
|
|
if (context.selected()) {
|
|
fillImageOverlay(p, rthumb, rounding, context);
|
|
}
|
|
|
|
// date
|
|
if (!_caption.isEmpty()) {
|
|
p.setPen(stm->historyTextFg);
|
|
_parent->prepareCustomEmojiPaint(p, context, _caption);
|
|
_caption.draw(p, {
|
|
.position = QPoint(
|
|
st::msgPadding.left(),
|
|
painty + painth + st::mediaCaptionSkip),
|
|
.availableWidth = captionw,
|
|
.palette = &stm->textPalette,
|
|
.pre = stm->preCache.get(),
|
|
.blockquote = stm->blockquoteCache.get(),
|
|
.colors = context.st->highlightColors(),
|
|
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
|
.now = context.now,
|
|
.pausedEmoji = context.paused || On(PowerSaving::kEmojiChat),
|
|
.pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler),
|
|
.selection = context.selection,
|
|
});
|
|
} else if (!inWebPage) {
|
|
auto fullRight = paintx + paintw;
|
|
auto fullBottom = painty + painth;
|
|
if (needInfoDisplay()) {
|
|
_parent->drawInfo(
|
|
p,
|
|
context,
|
|
fullRight,
|
|
fullBottom,
|
|
2 * paintx + paintw,
|
|
InfoDisplayType::Image);
|
|
}
|
|
if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
|
|
auto fastShareLeft = (fullRight + st::historyFastShareLeft);
|
|
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
|
|
_parent->drawRightAction(p, context, fastShareLeft, fastShareTop, 2 * paintx + paintw);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ExtendedPreview::validateImageCache(
|
|
QSize outer,
|
|
std::optional<Ui::BubbleRounding> rounding) const {
|
|
const auto ratio = style::DevicePixelRatio();
|
|
if (_spoiler.background.size() == (outer * ratio)
|
|
&& _spoiler.backgroundRounding == rounding) {
|
|
return;
|
|
}
|
|
_spoiler.background = Images::Round(
|
|
prepareImageCache(outer),
|
|
MediaRoundingMask(rounding));
|
|
_spoiler.backgroundRounding = rounding;
|
|
}
|
|
|
|
QImage ExtendedPreview::prepareImageCache(QSize outer) const {
|
|
ensureThumbnailRead();
|
|
return PrepareWithBlurredBackground(outer, {}, {}, _inlineThumbnail);
|
|
}
|
|
|
|
void ExtendedPreview::paintButton(
|
|
Painter &p,
|
|
QRect outer,
|
|
const PaintContext &context) const {
|
|
const auto st = context.st;
|
|
const auto &padding = st::extendedPreviewButtonPadding;
|
|
const auto margin = st::extendedPreviewButtonMargin;
|
|
const auto width = std::min(
|
|
_buttonText.maxWidth() + padding.left() + padding.right(),
|
|
outer.width() - 2 * margin);
|
|
const auto height = padding.top()
|
|
+ st::semiboldFont->height
|
|
+ padding.bottom();
|
|
const auto overlay = st->msgDateImgBg()->c;
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto size = QSize(width, height);
|
|
if (_buttonBackground.size() != size * ratio
|
|
|| _buttonBackgroundOverlay != overlay) {
|
|
auto &background = _spoiler.background;
|
|
if (background.width() < width * ratio
|
|
|| background.height() < height * ratio) {
|
|
return;
|
|
}
|
|
_buttonBackground = background.copy(QRect(
|
|
(background.width() - width * ratio) / 2,
|
|
(background.height() - height * ratio) / 2,
|
|
width * ratio,
|
|
height * ratio));
|
|
_buttonBackground.setDevicePixelRatio(ratio);
|
|
auto p = QPainter(&_buttonBackground);
|
|
p.fillRect(0, 0, width, height, overlay);
|
|
p.end();
|
|
_buttonBackground = Images::Round(
|
|
std::move(_buttonBackground),
|
|
Images::CornersMask(height / 2));
|
|
}
|
|
const auto left = outer.x() + (outer.width() - width) / 2;
|
|
const auto top = outer.y() + (outer.height() - height) / 2;
|
|
p.drawImage(left, top, _buttonBackground);
|
|
p.setPen(st->msgDateImgFg()->c);
|
|
_buttonText.drawLeftElided(
|
|
p,
|
|
left + padding.left(),
|
|
top + padding.top(),
|
|
width - padding.left() - padding.right(),
|
|
outer.width());
|
|
}
|
|
|
|
TextState ExtendedPreview::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();
|
|
auto bubble = _parent->hasBubble();
|
|
|
|
if (bubble && !_caption.isEmpty()) {
|
|
const auto captionw = paintw
|
|
- st::msgPadding.left()
|
|
- st::msgPadding.right();
|
|
painth -= _caption.countHeight(captionw);
|
|
if (isBubbleBottom()) {
|
|
painth -= st::msgPadding.bottom();
|
|
}
|
|
if (QRect(st::msgPadding.left(), painth, captionw, height() - painth).contains(point)) {
|
|
result = TextState(_parent, _caption.getState(
|
|
point - QPoint(st::msgPadding.left(), painth),
|
|
captionw,
|
|
request.forText()));
|
|
return result;
|
|
}
|
|
painth -= st::mediaCaptionSkip;
|
|
}
|
|
if (QRect(paintx, painty, paintw, painth).contains(point)) {
|
|
result.link = _spoiler.link;
|
|
}
|
|
if (_caption.isEmpty() && _parent->media() == this) {
|
|
auto fullRight = paintx + paintw;
|
|
auto fullBottom = painty + painth;
|
|
const auto bottomInfoResult = _parent->bottomInfoTextState(
|
|
fullRight,
|
|
fullBottom,
|
|
point,
|
|
InfoDisplayType::Image);
|
|
if (bottomInfoResult.link
|
|
|| bottomInfoResult.cursor != CursorState::None
|
|
|| bottomInfoResult.customTooltip) {
|
|
return bottomInfoResult;
|
|
}
|
|
if (const auto size = bubble ? std::nullopt : _parent->rightActionSize()) {
|
|
auto fastShareLeft = (fullRight + st::historyFastShareLeft);
|
|
auto fastShareTop = (fullBottom - st::historyFastShareBottom - size->height());
|
|
if (QRect(fastShareLeft, fastShareTop, size->width(), size->height()).contains(point)) {
|
|
result.link = _parent->rightActionLink(point
|
|
- QPoint(fastShareLeft, fastShareTop));
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool ExtendedPreview::toggleSelectionByHandlerClick(const ClickHandlerPtr &p) const {
|
|
return p == _spoiler.link;
|
|
}
|
|
|
|
bool ExtendedPreview::dragItemByHandler(const ClickHandlerPtr &p) const {
|
|
return p == _spoiler.link;
|
|
}
|
|
|
|
bool ExtendedPreview::needInfoDisplay() const {
|
|
return _parent->data()->isSending()
|
|
|| _parent->data()->hasFailed()
|
|
|| _parent->isUnderCursor()
|
|
|| _parent->isLastAndSelfMessage();
|
|
}
|
|
|
|
TextForMimeData ExtendedPreview::selectedText(TextSelection selection) const {
|
|
return _caption.toTextForMimeData(selection);
|
|
}
|
|
|
|
void ExtendedPreview::hideSpoilers() {
|
|
_caption.setSpoilerRevealed(false, anim::type::instant);
|
|
}
|
|
|
|
bool ExtendedPreview::needsBubble() const {
|
|
if (!_caption.isEmpty()) {
|
|
return true;
|
|
}
|
|
const auto item = _parent->data();
|
|
return !item->isService()
|
|
&& (item->repliesAreComments()
|
|
|| item->externalReply()
|
|
|| item->viaBot()
|
|
|| _parent->displayedReply()
|
|
|| _parent->displayForwardedFrom()
|
|
|| _parent->displayFromName()
|
|
|| _parent->displayedTopicButton());
|
|
}
|
|
|
|
QPoint ExtendedPreview::resolveCustomInfoRightBottom() const {
|
|
const auto skipx = (st::msgDateImgDelta + st::msgDateImgPadding.x());
|
|
const auto skipy = (st::msgDateImgDelta + st::msgDateImgPadding.y());
|
|
return QPoint(width() - skipx, height() - skipy);
|
|
}
|
|
|
|
void ExtendedPreview::parentTextUpdated() {
|
|
_caption = (_parent->media() == this)
|
|
? createCaption(_parent->data())
|
|
: Ui::Text::String();
|
|
history()->owner().requestViewResize(_parent);
|
|
}
|
|
|
|
} // namespace HistoryView
|