Optimize small emoji image caching and painting.

This commit is contained in:
John Preston 2021-12-20 15:30:01 +00:00
parent 4050866b3b
commit 6f8c9f65cf
7 changed files with 172 additions and 263 deletions

View file

@ -13,8 +13,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "base/timer_rpl.h"
#include "apiwrap.h"
#include "styles/style_chat.h"
namespace Data {
namespace {
@ -55,6 +58,105 @@ rpl::producer<> Reactions::updates() const {
return _updated.events();
}
void Reactions::preloadImageFor(const QString &emoji) {
if (_images.contains(emoji)) {
return;
}
auto &set = _images.emplace(emoji).first->second;
const auto i = ranges::find(_available, emoji, &Reaction::emoji);
const auto document = (i != end(_available))
? i->staticIcon.get()
: nullptr;
if (document) {
loadImage(set, document);
} else if (!_waitingForList) {
refresh();
}
}
QImage Reactions::resolveImageFor(
const QString &emoji,
ImageSize size) {
const auto i = _images.find(emoji);
if (i == end(_images)) {
preloadImageFor(emoji);
}
auto &set = (i != end(_images)) ? i->second : _images[emoji];
switch (size) {
case ImageSize::BottomInfo: return set.bottomInfo;
case ImageSize::InlineList: return set.inlineList;
}
Unexpected("ImageSize in Reactions::resolveImageFor.");
}
void Reactions::resolveImages() {
for (auto &[emoji, set] : _images) {
if (!set.bottomInfo.isNull() || set.media) {
continue;
}
const auto i = ranges::find(_available, emoji, &Reaction::emoji);
const auto document = (i != end(list()))
? i->staticIcon.get()
: nullptr;
if (document) {
loadImage(set, document);
} else {
LOG(("API Error: Reaction for emoji '%1' not found!"
).arg(emoji));
}
}
}
void Reactions::loadImage(
ImageSet &set,
not_null<DocumentData*> document) {
if (!set.bottomInfo.isNull()) {
return;
} else if (!set.media) {
set.media = document->createMediaView();
}
if (const auto image = set.media->getStickerLarge()) {
setImage(set, image->original());
} else if (!_imagesLoadLifetime) {
document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
downloadTaskFinished();
}, _imagesLoadLifetime);
}
}
void Reactions::setImage(ImageSet &set, QImage large) {
set.media = nullptr;
const auto scale = [&](int size) {
const auto factor = style::DevicePixelRatio();
return Images::prepare(
large,
size * factor,
size * factor,
Images::Option::Smooth,
size,
size);
};
set.bottomInfo = scale(st::reactionInfoSize);
set.inlineList = scale(st::reactionBottomSize);
}
void Reactions::downloadTaskFinished() {
auto hasOne = false;
for (auto &[emoji, set] : _images) {
if (!set.media) {
continue;
} else if (const auto image = set.media->getStickerLarge()) {
setImage(set, image->original());
} else {
hasOne = true;
}
}
if (!hasOne) {
_imagesLoadLifetime.destroy();
}
}
std::vector<Reaction> Reactions::Filtered(
const std::vector<Reaction> &reactions,
const std::vector<QString> &emoji) {
@ -101,6 +203,10 @@ void Reactions::request() {
_available.push_back(*parsed);
}
}
if (_waitingForList) {
_waitingForList = false;
resolveImages();
}
_updated.fire({});
}, [&](const MTPDmessages_availableReactionsNotModified &) {
});

View file

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Data {
class DocumentMedia;
class Session;
struct Reaction {
@ -40,12 +41,32 @@ public:
[[nodiscard]] rpl::producer<> updates() const;
enum class ImageSize {
BottomInfo,
InlineList,
};
void preloadImageFor(const QString &emoji);
[[nodiscard]] QImage resolveImageFor(
const QString &emoji,
ImageSize size);
private:
struct ImageSet {
QImage bottomInfo;
QImage inlineList;
std::shared_ptr<DocumentMedia> media;
};
void request();
[[nodiscard]] std::optional<Reaction> parse(
const MTPAvailableReaction &entry);
void loadImage(ImageSet &set, not_null<DocumentData*> document);
void setImage(ImageSet &set, QImage large);
void resolveImages();
void downloadTaskFinished();
const not_null<Session*> _owner;
std::vector<Reaction> _available;
@ -54,6 +75,10 @@ private:
mtpRequestId _requestId = 0;
int32 _hash = 0;
base::flat_map<QString, ImageSet> _images;
rpl::lifetime _imagesLoadLifetime;
bool _waitingForList = false;
rpl::lifetime _lifetime;
};

View file

@ -18,17 +18,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "main/main_session.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"
namespace HistoryView {
BottomInfo::BottomInfo(Data &&data)
: _data(std::move(data)) {
BottomInfo::BottomInfo(
not_null<::Data::Reactions*> reactionsOwner,
Data &&data)
: _reactionsOwner(reactionsOwner)
, _data(std::move(data)) {
layout();
}
@ -227,6 +226,11 @@ void BottomInfo::paintReactions(
y += st::msgDateFont->height;
widthLeft = availableWidth;
}
if (reaction.image.isNull()) {
reaction.image = _reactionsOwner->resolveImageFor(
reaction.emoji,
::Data::Reactions::ImageSize::BottomInfo);
}
if (!reaction.image.isNull()) {
p.drawImage(
x,
@ -362,56 +366,10 @@ QSize BottomInfo::countOptimalSize() {
BottomInfo::Reaction BottomInfo::prepareReactionWithEmoji(
const QString &emoji) {
auto result = Reaction{ .emoji = emoji };
auto &reactions = _data.owner->reactions();
const auto &list = reactions.list();
const auto i = ranges::find(
list,
emoji,
&::Data::Reaction::emoji);
const auto document = (i != end(list))
? i->staticIcon.get()
: nullptr;
if (document) {
loadReactionImage(result, document);
} else if (!_waitingForReactionsList) {
reactions.refresh();
reactions.updates(
) | rpl::filter([=] {
return _waitingForReactionsList;
}) | rpl::start_with_next([=] {
reactionsListLoaded();
}, _assetsLoadLifetime);
}
_reactionsOwner->preloadImageFor(emoji);
return result;
}
void BottomInfo::reactionsListLoaded() {
_waitingForReactionsList = false;
if (assetsLoaded()) {
_assetsLoadLifetime.destroy();
}
const auto &list = _data.owner->reactions().list();
for (auto &reaction : _reactions) {
if (!reaction.image.isNull() || reaction.media) {
continue;
}
const auto i = ranges::find(
list,
reaction.emoji,
&::Data::Reaction::emoji);
const auto document = (i != end(list))
? i->staticIcon.get()
: nullptr;
if (document) {
loadReactionImage(reaction, document);
} else {
LOG(("API Error: Reaction for emoji '%1' not found!"
).arg(reaction.emoji));
}
}
}
void BottomInfo::setReactionCount(Reaction &reaction, int count) {
if (reaction.count == count) {
return;
@ -425,72 +383,14 @@ void BottomInfo::setReactionCount(Reaction &reaction, int count) {
: 0;
}
void BottomInfo::loadReactionImage(
Reaction &reaction,
not_null<DocumentData*> document) {
if (!reaction.image.isNull()) {
return;
} else if (!reaction.media) {
reaction.media = document->createMediaView();
}
if (const auto image = reaction.media->getStickerLarge()) {
setReactionImage(reaction, image->original());
} else if (!_waitingForDownloadTask) {
_waitingForDownloadTask = true;
document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
downloadTaskFinished();
}, _assetsLoadLifetime);
}
}
void BottomInfo::setReactionImage(Reaction &reaction, QImage large) {
reaction.media = nullptr;
const auto size = st::reactionInfoSize;
const auto factor = style::DevicePixelRatio();
reaction.image = Images::prepare(
std::move(large),
size * factor,
size * factor,
Images::Option::Smooth,
size,
size);
}
void BottomInfo::downloadTaskFinished() {
auto hasOne = false;
for (auto &reaction : _reactions) {
if (!reaction.media) {
continue;
} else if (const auto image = reaction.media->getStickerLarge()) {
setReactionImage(reaction, image->original());
} else {
hasOne = true;
}
}
if (!hasOne) {
_waitingForDownloadTask = false;
if (assetsLoaded()) {
_assetsLoadLifetime.destroy();
}
}
}
bool BottomInfo::assetsLoaded() const {
return !_waitingForReactionsList && !_waitingForDownloadTask;
}
BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
using Flag = BottomInfo::Data::Flag;
const auto owner = &message->data()->history()->owner();
auto result = BottomInfo::Data{ .owner = owner };
const auto item = message->message();
auto result = BottomInfo::Data();
result.date = message->dateTime();
if (message->embedReactionsInBottomInfo()) {
result.reactions = item->reactions();
result.chosenReaction = item->chosenReaction();
}
if (message->hasOutLayout()) {
result.flags |= Flag::OutLayout;

View file

@ -16,8 +16,7 @@ struct ChatPaintContext;
} // namespace Ui
namespace Data {
class Session;
class DocumentMedia;
class Reactions;
} // namespace Data
namespace HistoryView {
@ -41,16 +40,14 @@ public:
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
not_null<::Data::Session*> owner;
QDateTime date;
QString author;
base::flat_map<QString, int> reactions;
QString chosenReaction;
std::optional<int> views;
std::optional<int> replies;
Flags flags;
};
explicit BottomInfo(Data &&data);
BottomInfo(not_null<::Data::Reactions*> reactionsOwner, Data &&data);
void update(Data &&data, int availableWidth);
@ -70,9 +67,8 @@ public:
private:
struct Reaction {
QImage image;
mutable QImage image;
QString emoji;
std::shared_ptr<::Data::DocumentMedia> media;
QString countText;
int count = 0;
int countTextWidth = 0;
@ -96,14 +92,9 @@ private:
QSize countCurrentSize(int newWidth) override;
void setReactionCount(Reaction &reaction, int count);
void loadReactionImage(Reaction &reaction, not_null<DocumentData*> document);
void setReactionImage(Reaction &reaction, QImage large);
[[nodiscard]] Reaction prepareReactionWithEmoji(const QString &emoji);
void reactionsListLoaded();
void downloadTaskFinished();
[[nodiscard]] bool assetsLoaded() const;
const not_null<::Data::Reactions*> _reactionsOwner;
Data _data;
Ui::Text::String _authorEditedDate;
Ui::Text::String _views;
@ -111,11 +102,6 @@ private:
std::vector<Reaction> _reactions;
int _reactionsMaxWidth = 0;
int _dateWidth = 0;
rpl::lifetime _assetsLoadLifetime;
bool _waitingForReactionsList = false;
bool _waitingForDownloadTask = false;
bool _authorElided = false;
};

View file

@ -245,7 +245,9 @@ Message::Message(
not_null<HistoryMessage*> data,
Element *replacing)
: Element(delegate, data, replacing)
, _bottomInfo(BottomInfoDataFromMessage(this)) {
, _bottomInfo(
&data->history()->owner().reactions(),
BottomInfoDataFromMessage(this)) {
initLogEntryOriginal();
initPsa();
refreshReactions();
@ -1924,7 +1926,9 @@ void Message::refreshReactions() {
using namespace Reactions;
auto data = InlineListDataFromMessage(this);
if (!_reactions) {
_reactions = std::make_unique<InlineList>(std::move(data));
_reactions = std::make_unique<InlineList>(
&item->history()->owner().reactions(),
std::move(data));
} else {
_reactions->update(std::move(data), width());
}

View file

@ -10,11 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_message.h"
#include "history/history.h"
#include "history/view/history_view_message.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_message_reactions.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "lang/lang_tag.h"
#include "ui/chat/chat_style.h"
#include "styles/style_chat.h"
@ -27,8 +23,9 @@ constexpr auto kOutNonChosenOpacity = 0.18;
} // namespace
InlineList::InlineList(Data &&data)
: _data(std::move(data)) {
InlineList::InlineList(not_null<::Data::Reactions*> owner, Data &&data)
: _owner(owner)
, _data(std::move(data)) {
layout();
}
@ -79,56 +76,10 @@ void InlineList::layoutButtons() {
InlineList::Button InlineList::prepareButtonWithEmoji(const QString &emoji) {
auto result = Button{ .emoji = emoji };
auto &reactions = _data.owner->reactions();
const auto &list = reactions.list();
const auto i = ranges::find(
list,
emoji,
&::Data::Reaction::emoji);
const auto document = (i != end(list))
? i->staticIcon.get()
: nullptr;
if (document) {
loadButtonImage(result, document);
} else if (!_waitingForReactionsList) {
reactions.refresh();
reactions.updates(
) | rpl::filter([=] {
return _waitingForReactionsList;
}) | rpl::start_with_next([=] {
reactionsListLoaded();
}, _assetsLoadLifetime);
}
_owner->preloadImageFor(emoji);
return result;
}
void InlineList::reactionsListLoaded() {
_waitingForReactionsList = false;
if (assetsLoaded()) {
_assetsLoadLifetime.destroy();
}
const auto &list = _data.owner->reactions().list();
for (auto &button : _buttons) {
if (!button.image.isNull() || button.media) {
continue;
}
const auto i = ranges::find(
list,
button.emoji,
&::Data::Reaction::emoji);
const auto document = (i != end(list))
? i->staticIcon.get()
: nullptr;
if (document) {
loadButtonImage(button, document);
} else {
LOG(("API Error: Reaction for emoji '%1' not found!"
).arg(button.emoji));
}
}
}
void InlineList::setButtonCount(Button &button, int count) {
if (button.count == count) {
return;
@ -138,61 +89,6 @@ void InlineList::setButtonCount(Button &button, int count) {
button.countTextWidth = st::semiboldFont->width(button.countText);
}
void InlineList::loadButtonImage(
Button &button,
not_null<DocumentData*> document) {
if (!button.image.isNull()) {
return;
} else if (!button.media) {
button.media = document->createMediaView();
}
if (const auto image = button.media->getStickerLarge()) {
setButtonImage(button, image->original());
} else if (!_waitingForDownloadTask) {
_waitingForDownloadTask = true;
document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
downloadTaskFinished();
}, _assetsLoadLifetime);
}
}
void InlineList::setButtonImage(Button &button, QImage large) {
button.media = nullptr;
const auto size = st::reactionBottomSize;
const auto factor = style::DevicePixelRatio();
button.image = Images::prepare(
std::move(large),
size * factor,
size * factor,
Images::Option::Smooth,
size,
size);
}
void InlineList::downloadTaskFinished() {
auto hasOne = false;
for (auto &button : _buttons) {
if (!button.media) {
continue;
} else if (const auto image = button.media->getStickerLarge()) {
setButtonImage(button, image->original());
} else {
hasOne = true;
}
}
if (!hasOne) {
_waitingForDownloadTask = false;
if (assetsLoaded()) {
_assetsLoadLifetime.destroy();
}
}
}
bool InlineList::assetsLoaded() const {
return !_waitingForReactionsList && !_waitingForDownloadTask;
}
QSize InlineList::countOptimalSize() {
if (_buttons.empty()) {
return _skipBlock;
@ -282,7 +178,14 @@ void InlineList::paint(
p.setOpacity(1.);
}
}
p.drawImage(inner.topLeft(), button.image);
if (button.image.isNull()) {
button.image = _owner->resolveImageFor(
button.emoji,
::Data::Reactions::ImageSize::InlineList);
}
if (!button.image.isNull()) {
p.drawImage(inner.topLeft(), button.image);
}
p.setPen(!inbubble
? st->msgServiceFg()
: !chosen
@ -304,11 +207,10 @@ void InlineList::paint(
}
InlineListData InlineListDataFromMessage(not_null<Message*> message) {
const auto owner = &message->data()->history()->owner();
auto result = InlineListData{ .owner = owner };
using Flag = InlineListData::Flag;
const auto item = message->message();
auto result = InlineListData();
result.reactions = item->reactions();
result.chosenReaction = item->chosenReaction();
result.flags = (message->hasOutLayout() ? Flag::OutLayout : Flag())

View file

@ -9,11 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_object.h"
class DocumentData;
namespace Data {
class Session;
class DocumentMedia;
class Reactions;
} // namespace Data
namespace Ui {
@ -35,7 +32,6 @@ struct InlineListData {
friend inline constexpr bool is_flag_type(Flag) { return true; };
using Flags = base::flags<Flag>;
not_null<Data::Session*> owner;
base::flat_map<QString, int> reactions;
QString chosenReaction;
Flags flags = {};
@ -44,7 +40,7 @@ struct InlineListData {
class InlineList final : public Object {
public:
using Data = InlineListData;
explicit InlineList(Data &&data);
InlineList(not_null<::Data::Reactions*> owner, Data &&data);
void update(Data &&data, int availableWidth);
QSize countCurrentSize(int newWidth) override;
@ -61,9 +57,8 @@ public:
private:
struct Button {
QRect geometry;
QImage image;
mutable QImage image;
QString emoji;
std::shared_ptr<::Data::DocumentMedia> media;
ClickHandlerPtr link;
QString countText;
int count = 0;
@ -74,24 +69,15 @@ private:
void layoutButtons();
void setButtonCount(Button &button, int count);
void loadButtonImage(Button &button, not_null<DocumentData*> document);
void setButtonImage(Button &button, QImage large);
[[nodiscard]] Button prepareButtonWithEmoji(const QString &emoji);
void reactionsListLoaded();
void downloadTaskFinished();
[[nodiscard]] bool assetsLoaded() const;
QSize countOptimalSize() override;
const not_null<::Data::Reactions*> _owner;
Data _data;
std::vector<Button> _buttons;
QSize _skipBlock;
rpl::lifetime _assetsLoadLifetime;
bool _waitingForReactionsList = false;
bool _waitingForDownloadTask = false;
};
[[nodiscard]] InlineListData InlineListDataFromMessage(