570 lines
15 KiB
C++
570 lines
15 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_sticker.h"
|
|
|
|
#include "boxes/sticker_set_box.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item_components.h"
|
|
#include "history/history_item.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 "ui/image/image.h"
|
|
#include "ui/chat/chat_style.h"
|
|
#include "ui/effects/path_shift_gradient.h"
|
|
#include "ui/emoji_config.h"
|
|
#include "core/application.h"
|
|
#include "core/core_settings.h"
|
|
#include "core/click_handler_types.h"
|
|
#include "main/main_session.h"
|
|
#include "main/main_account.h"
|
|
#include "main/main_app_config.h"
|
|
#include "window/window_session_controller.h" // isGifPausedAtLeastFor.
|
|
#include "data/data_session.h"
|
|
#include "data/data_document.h"
|
|
#include "data/data_document_media.h"
|
|
#include "data/data_file_click_handler.h"
|
|
#include "data/data_file_origin.h"
|
|
#include "lottie/lottie_single_player.h"
|
|
#include "chat_helpers/stickers_gift_box_pack.h"
|
|
#include "chat_helpers/stickers_lottie.h"
|
|
#include "styles/style_chat.h"
|
|
|
|
namespace HistoryView {
|
|
namespace {
|
|
|
|
constexpr auto kMaxSizeFixed = 512;
|
|
constexpr auto kMaxEmojiSizeFixed = 256;
|
|
constexpr auto kPremiumMultiplier = (1 + 0.245 * 2);
|
|
constexpr auto kEmojiMultiplier = 3;
|
|
|
|
[[nodiscard]] QImage CacheDiceImage(
|
|
const QString &emoji,
|
|
int index,
|
|
const QImage &image) {
|
|
static auto Cache = base::flat_map<std::pair<QString, int>, QImage>();
|
|
const auto key = std::make_pair(emoji, index);
|
|
const auto i = Cache.find(key);
|
|
if (i != end(Cache) && i->second.size() == image.size()) {
|
|
return i->second;
|
|
}
|
|
Cache[key] = image;
|
|
return image;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Sticker::Sticker(
|
|
not_null<Element*> parent,
|
|
not_null<DocumentData*> data,
|
|
bool skipPremiumEffect,
|
|
Element *replacing,
|
|
const Lottie::ColorReplacements *replacements)
|
|
: _parent(parent)
|
|
, _data(data)
|
|
, _replacements(replacements)
|
|
, _cachingTag(ChatHelpers::StickerLottieSize::MessageHistory)
|
|
, _lottieOncePlayed(false)
|
|
, _premiumEffectPlayed(false)
|
|
, _nextLastDiceFrame(false)
|
|
, _skipPremiumEffect(skipPremiumEffect) {
|
|
if ((_dataMedia = _data->activeMediaView())) {
|
|
dataMediaCreated();
|
|
} else {
|
|
_data->loadThumbnail(parent->data()->fullId());
|
|
if (hasPremiumEffect()) {
|
|
_data->loadVideoThumbnail(parent->data()->fullId());
|
|
}
|
|
}
|
|
if (const auto media = replacing ? replacing->media() : nullptr) {
|
|
_lottie = media->stickerTakeLottie(_data, _replacements);
|
|
if (_lottie) {
|
|
//_externalInfo = media->externalLottieInfo();
|
|
if (hasPremiumEffect() && !_premiumEffectPlayed) {
|
|
_premiumEffectPlayed = true;
|
|
_parent->delegate()->elementStartPremium(_parent, replacing);
|
|
}
|
|
lottieCreated();
|
|
}
|
|
}
|
|
}
|
|
|
|
Sticker::~Sticker() {
|
|
if (_lottie || _dataMedia) {
|
|
if (_lottie) {
|
|
unloadLottie();
|
|
}
|
|
if (_dataMedia) {
|
|
_data->owner().keepAlive(base::take(_dataMedia));
|
|
_parent->checkHeavyPart();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Sticker::hasPremiumEffect() const {
|
|
return !_skipPremiumEffect && _data->isPremiumSticker();
|
|
}
|
|
|
|
bool Sticker::customEmojiPart() const {
|
|
return (_cachingTag != ChatHelpers::StickerLottieSize::MessageHistory);
|
|
}
|
|
|
|
bool Sticker::isEmojiSticker() const {
|
|
return (_parent->data()->media() == nullptr);
|
|
}
|
|
|
|
void Sticker::initSize() {
|
|
if (isEmojiSticker() || _diceIndex >= 0) {
|
|
const auto &session = _data->owner().session();
|
|
if (session.giftBoxStickersPacks().isGiftSticker(_data)) {
|
|
_size = st::msgServiceGiftBoxStickerSize;
|
|
} else {
|
|
_size = EmojiSize();
|
|
}
|
|
if (_diceIndex > 0) {
|
|
[[maybe_unused]] bool result = readyToDrawLottie();
|
|
}
|
|
} else {
|
|
_size = Size(_data);
|
|
}
|
|
}
|
|
|
|
QSize Sticker::countOptimalSize() {
|
|
if (_size.isEmpty()) {
|
|
initSize();
|
|
}
|
|
return DownscaledSize(_size, Size());
|
|
}
|
|
|
|
bool Sticker::readyToDrawLottie() {
|
|
if (!_lastDiceFrame.isNull()) {
|
|
return true;
|
|
}
|
|
const auto sticker = _data->sticker();
|
|
if (!sticker) {
|
|
return false;
|
|
}
|
|
|
|
ensureDataMediaCreated();
|
|
_dataMedia->checkStickerLarge();
|
|
const auto loaded = _dataMedia->loaded();
|
|
const auto waitingForPremium = hasPremiumEffect()
|
|
&& _dataMedia->videoThumbnailContent().isEmpty();
|
|
if (sticker->isLottie() && !_lottie && loaded && !waitingForPremium) {
|
|
setupLottie();
|
|
}
|
|
return (_lottie && _lottie->ready());
|
|
}
|
|
|
|
QSize Sticker::Size() {
|
|
const auto side = std::min(st::maxStickerSize, kMaxSizeFixed);
|
|
return { side, side };
|
|
}
|
|
|
|
QSize Sticker::Size(not_null<DocumentData*> document) {
|
|
return DownscaledSize(document->dimensions, Size());
|
|
}
|
|
|
|
QSize Sticker::PremiumEffectSize(not_null<DocumentData*> document) {
|
|
return Size(document) * kPremiumMultiplier;
|
|
}
|
|
|
|
QSize Sticker::UsualPremiumEffectSize() {
|
|
return DownscaledSize({ kMaxSizeFixed, kMaxSizeFixed }, Size())
|
|
* kPremiumMultiplier;
|
|
}
|
|
|
|
QSize Sticker::EmojiEffectSize() {
|
|
return EmojiSize() * kEmojiMultiplier;
|
|
}
|
|
|
|
QSize Sticker::EmojiSize() {
|
|
const auto side = std::min(st::maxAnimatedEmojiSize, kMaxEmojiSizeFixed);
|
|
return { side, side };
|
|
}
|
|
|
|
void Sticker::draw(
|
|
Painter &p,
|
|
const PaintContext &context,
|
|
const QRect &r) {
|
|
if (!customEmojiPart() && isEmojiSticker()) {
|
|
_parent->clearCustomEmojiRepaint();
|
|
}
|
|
|
|
ensureDataMediaCreated();
|
|
if (readyToDrawLottie()) {
|
|
paintLottie(p, context, r);
|
|
} else if (!_data->sticker()
|
|
|| (_data->sticker()->isLottie() && _replacements)
|
|
|| !paintPixmap(p, context, r)) {
|
|
paintPath(p, context, r);
|
|
}
|
|
}
|
|
|
|
ClickHandlerPtr Sticker::link() {
|
|
return _link;
|
|
}
|
|
|
|
DocumentData *Sticker::document() {
|
|
return _data;
|
|
}
|
|
|
|
void Sticker::stickerClearLoopPlayed() {
|
|
_lottieOncePlayed = false;
|
|
_premiumEffectPlayed = false;
|
|
}
|
|
|
|
void Sticker::paintLottie(
|
|
Painter &p,
|
|
const PaintContext &context,
|
|
const QRect &r) {
|
|
auto request = Lottie::FrameRequest();
|
|
request.box = _size * cIntRetinaFactor();
|
|
if (context.selected() && !_nextLastDiceFrame) {
|
|
request.colored = context.st->msgStickerOverlay()->c;
|
|
}
|
|
request.mirrorHorizontal = mirrorHorizontal();
|
|
const auto frame = _lottie
|
|
? _lottie->frameInfo(request)
|
|
: Lottie::Animation::FrameInfo();
|
|
if (_nextLastDiceFrame) {
|
|
_nextLastDiceFrame = false;
|
|
_lastDiceFrame = CacheDiceImage(_diceEmoji, _diceIndex, frame.image);
|
|
}
|
|
const auto &image = _lastDiceFrame.isNull()
|
|
? frame.image
|
|
: _lastDiceFrame;
|
|
const auto prepared = (!_lastDiceFrame.isNull() && context.selected())
|
|
? Images::Colored(
|
|
base::duplicate(image),
|
|
context.st->msgStickerOverlay()->c)
|
|
: image;
|
|
const auto size = prepared.size() / cIntRetinaFactor();
|
|
p.drawImage(
|
|
QRect(
|
|
QPoint(
|
|
r.x() + (r.width() - size.width()) / 2,
|
|
r.y() + (r.height() - size.height()) / 2),
|
|
size),
|
|
prepared);
|
|
if (!_lastDiceFrame.isNull()) {
|
|
return;
|
|
}
|
|
|
|
const auto count = _lottie->information().framesCount;
|
|
_frameIndex = frame.index;
|
|
_framesCount = count;
|
|
const auto paused = /*(_externalInfo.frame >= 0)
|
|
? (_frameIndex % _externalInfo.count >= _externalInfo.frame)
|
|
: */_parent->delegate()->elementIsGifPaused();
|
|
_nextLastDiceFrame = !paused
|
|
&& (_diceIndex > 0)
|
|
&& (_frameIndex + 2 == count);
|
|
const auto playOnce = (_diceIndex > 0)
|
|
? true
|
|
: (_diceIndex == 0)
|
|
? false
|
|
: ((!customEmojiPart() && isEmojiSticker())
|
|
|| !Core::App().settings().loopAnimatedStickers());
|
|
const auto lastDiceFrame = (_diceIndex > 0) && atTheEnd();
|
|
const auto switchToNext = /*(_externalInfo.frame >= 0)
|
|
|| */!playOnce
|
|
|| (!lastDiceFrame && (_frameIndex != 0 || !_lottieOncePlayed));
|
|
if (!paused
|
|
&& switchToNext
|
|
&& _lottie->markFrameShown()
|
|
&& playOnce
|
|
&& !_lottieOncePlayed) {
|
|
_lottieOncePlayed = true;
|
|
_parent->delegate()->elementStartStickerLoop(_parent);
|
|
}
|
|
checkPremiumEffectStart();
|
|
}
|
|
|
|
bool Sticker::paintPixmap(
|
|
Painter &p,
|
|
const PaintContext &context,
|
|
const QRect &r) {
|
|
const auto pixmap = paintedPixmap(context);
|
|
if (pixmap.isNull()) {
|
|
return false;
|
|
}
|
|
const auto position = QPoint(
|
|
r.x() + (r.width() - _size.width()) / 2,
|
|
r.y() + (r.height() - _size.height()) / 2);
|
|
const auto size = pixmap.size() / pixmap.devicePixelRatio();
|
|
const auto mirror = mirrorHorizontal();
|
|
if (mirror) {
|
|
p.save();
|
|
const auto middle = QPointF(
|
|
position.x() + size.width() / 2.,
|
|
position.y() + size.height() / 2.);
|
|
p.translate(middle);
|
|
p.scale(-1., 1.);
|
|
p.translate(-middle);
|
|
}
|
|
p.drawPixmap(position, pixmap);
|
|
if (mirror) {
|
|
p.restore();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Sticker::paintPath(
|
|
Painter &p,
|
|
const PaintContext &context,
|
|
const QRect &r) {
|
|
const auto pathGradient = _parent->delegate()->elementPathShiftGradient();
|
|
if (context.selected()) {
|
|
pathGradient->overrideColors(
|
|
context.st->msgServiceBgSelected(),
|
|
context.st->msgServiceBg());
|
|
} else {
|
|
pathGradient->clearOverridenColors();
|
|
}
|
|
p.setBrush(context.imageStyle()->msgServiceBg);
|
|
ChatHelpers::PaintStickerThumbnailPath(
|
|
p,
|
|
_dataMedia.get(),
|
|
r,
|
|
pathGradient,
|
|
mirrorHorizontal());
|
|
}
|
|
|
|
QPixmap Sticker::paintedPixmap(const PaintContext &context) const {
|
|
const auto colored = context.selected()
|
|
? &context.st->msgStickerOverlay()
|
|
: nullptr;
|
|
const auto good = _dataMedia->goodThumbnail();
|
|
if (const auto image = _dataMedia->getStickerLarge()) {
|
|
return image->pix(_size, { .colored = colored });
|
|
//
|
|
// Inline thumbnails can't have alpha channel.
|
|
//
|
|
//} else if (const auto blurred = _data->thumbnailInline()) {
|
|
// return blurred->pix(
|
|
// _size,
|
|
// { .colored = colored, .options = Images::Option::Blur });
|
|
} else if (good) {
|
|
return good->pix(_size, { .colored = colored });
|
|
} else if (const auto thumbnail = _dataMedia->thumbnail()) {
|
|
return thumbnail->pix(
|
|
_size,
|
|
{ .colored = colored, .options = Images::Option::Blur });
|
|
}
|
|
return QPixmap();
|
|
}
|
|
|
|
bool Sticker::mirrorHorizontal() const {
|
|
if (!hasPremiumEffect()) {
|
|
return false;
|
|
}
|
|
const auto rightAligned = _parent->hasOutLayout()
|
|
&& !_parent->delegate()->elementIsChatWide();
|
|
return !rightAligned;
|
|
}
|
|
|
|
ClickHandlerPtr Sticker::ShowSetHandler(not_null<DocumentData*> document) {
|
|
return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
|
|
const auto my = context.other.value<ClickHandlerContext>();
|
|
if (const auto window = my.sessionWindow.get()) {
|
|
StickerSetBox::Show(window, document);
|
|
}
|
|
});
|
|
}
|
|
|
|
void Sticker::refreshLink() {
|
|
if (_link) {
|
|
return;
|
|
}
|
|
const auto sticker = _data->sticker();
|
|
if (isEmojiSticker()) {
|
|
const auto weak = base::make_weak(this);
|
|
_link = std::make_shared<LambdaClickHandler>([weak] {
|
|
if (const auto that = weak.get()) {
|
|
that->emojiStickerClicked();
|
|
}
|
|
});
|
|
} else if (sticker && sticker->set) {
|
|
if (hasPremiumEffect()) {
|
|
const auto weak = base::make_weak(this);
|
|
_link = std::make_shared<LambdaClickHandler>([weak] {
|
|
if (const auto that = weak.get()) {
|
|
that->premiumStickerClicked();
|
|
}
|
|
});
|
|
} else {
|
|
_link = ShowSetHandler(_data);
|
|
}
|
|
} else if (sticker
|
|
&& (_data->dimensions.width() > kStickerSideSize
|
|
|| _data->dimensions.height() > kStickerSideSize)
|
|
&& !_parent->data()->isSending()
|
|
&& !_parent->data()->hasFailed()) {
|
|
// In case we have a .webp file that is displayed as a sticker, but
|
|
// that doesn't fit in 512x512, we assume it may be a regular large
|
|
// .webp image and we allow to open it in media viewer.
|
|
_link = std::make_shared<DocumentOpenClickHandler>(
|
|
_data,
|
|
crl::guard(this, [=](FullMsgId id) {
|
|
_parent->delegate()->elementOpenDocument(_data, id);
|
|
}),
|
|
_parent->data()->fullId());
|
|
}
|
|
}
|
|
|
|
void Sticker::emojiStickerClicked() {
|
|
if (_lottie) {
|
|
_parent->delegate()->elementStartInteraction(_parent);
|
|
}
|
|
_lottieOncePlayed = false;
|
|
_parent->history()->owner().requestViewRepaint(_parent);
|
|
}
|
|
|
|
void Sticker::premiumStickerClicked() {
|
|
_premiumEffectPlayed = false;
|
|
_parent->history()->owner().requestViewRepaint(_parent);
|
|
}
|
|
|
|
void Sticker::ensureDataMediaCreated() const {
|
|
if (_dataMedia) {
|
|
return;
|
|
}
|
|
_dataMedia = _data->createMediaView();
|
|
dataMediaCreated();
|
|
}
|
|
|
|
void Sticker::dataMediaCreated() const {
|
|
Expects(_dataMedia != nullptr);
|
|
|
|
_dataMedia->goodThumbnailWanted();
|
|
if (_dataMedia->thumbnailPath().isEmpty()) {
|
|
_dataMedia->thumbnailWanted(_parent->data()->fullId());
|
|
}
|
|
if (hasPremiumEffect()) {
|
|
_data->loadVideoThumbnail(_parent->data()->fullId());
|
|
}
|
|
_parent->history()->owner().registerHeavyViewPart(_parent);
|
|
}
|
|
|
|
void Sticker::setDiceIndex(const QString &emoji, int index) {
|
|
_diceEmoji = emoji;
|
|
_diceIndex = index;
|
|
}
|
|
|
|
void Sticker::setCustomEmojiPart(
|
|
int size,
|
|
ChatHelpers::StickerLottieSize tag) {
|
|
_size = { size, size };
|
|
_cachingTag = tag;
|
|
}
|
|
|
|
void Sticker::setupLottie() {
|
|
Expects(_dataMedia != nullptr);
|
|
|
|
_lottie = ChatHelpers::LottiePlayerFromDocument(
|
|
_dataMedia.get(),
|
|
_replacements,
|
|
_cachingTag,
|
|
countOptimalSize() * style::DevicePixelRatio(),
|
|
Lottie::Quality::High);
|
|
checkPremiumEffectStart();
|
|
lottieCreated();
|
|
}
|
|
|
|
void Sticker::checkPremiumEffectStart() {
|
|
if (!_premiumEffectPlayed && hasPremiumEffect()) {
|
|
_premiumEffectPlayed = true;
|
|
_parent->delegate()->elementStartPremium(_parent, nullptr);
|
|
}
|
|
}
|
|
|
|
void Sticker::lottieCreated() {
|
|
Expects(_lottie != nullptr);
|
|
|
|
_parent->history()->owner().registerHeavyViewPart(_parent);
|
|
|
|
_lottie->updates(
|
|
) | rpl::start_with_next([=](Lottie::Update update) {
|
|
v::match(update.data, [&](const Lottie::Information &information) {
|
|
_parent->customEmojiRepaint();
|
|
//markFramesTillExternal();
|
|
}, [&](const Lottie::DisplayFrameRequest &request) {
|
|
_parent->customEmojiRepaint();
|
|
});
|
|
}, _lifetime);
|
|
}
|
|
|
|
bool Sticker::hasHeavyPart() const {
|
|
return _lottie || _dataMedia;
|
|
}
|
|
|
|
void Sticker::unloadHeavyPart() {
|
|
unloadLottie();
|
|
_dataMedia = nullptr;
|
|
}
|
|
|
|
void Sticker::unloadLottie() {
|
|
if (!_lottie) {
|
|
return;
|
|
}
|
|
if (_diceIndex > 0 && _lastDiceFrame.isNull()) {
|
|
_nextLastDiceFrame = false;
|
|
_lottieOncePlayed = false;
|
|
}
|
|
_lottie = nullptr;
|
|
if (hasPremiumEffect()) {
|
|
_parent->delegate()->elementCancelPremium(_parent);
|
|
}
|
|
_parent->checkHeavyPart();
|
|
}
|
|
|
|
std::unique_ptr<Lottie::SinglePlayer> Sticker::stickerTakeLottie(
|
|
not_null<DocumentData*> data,
|
|
const Lottie::ColorReplacements *replacements) {
|
|
return (data == _data && replacements == _replacements)
|
|
? std::move(_lottie)
|
|
: nullptr;
|
|
}
|
|
|
|
//void Sticker::externalLottieProgressing(bool external) {
|
|
// _externalInfo = !external
|
|
// ? ExternalLottieInfo{}
|
|
// : (_externalInfo.frame > 0)
|
|
// ? _externalInfo
|
|
// : ExternalLottieInfo{ 0, 2 };
|
|
//}
|
|
//
|
|
//bool Sticker::externalLottieTill(ExternalLottieInfo info) {
|
|
// if (_externalInfo.frame >= 0) {
|
|
// _externalInfo = info;
|
|
// }
|
|
// return markFramesTillExternal();
|
|
//}
|
|
//
|
|
//ExternalLottieInfo Sticker::externalLottieInfo() const {
|
|
// return _externalInfo;
|
|
//}
|
|
//
|
|
//bool Sticker::markFramesTillExternal() {
|
|
// if (_externalInfo.frame < 0 || !_lottie) {
|
|
// return true;
|
|
// } else if (!_lottie->ready()) {
|
|
// return false;
|
|
// }
|
|
// const auto till = _externalInfo.frame % _lottie->framesCount();
|
|
// while (_lottie->frameIndex() < till) {
|
|
// if (!_lottie->markFrameShown()) {
|
|
// return false;
|
|
// }
|
|
// }
|
|
// return true;
|
|
//}
|
|
|
|
} // namespace HistoryView
|