Nice reactions design in private chats.

This commit is contained in:
John Preston 2021-12-20 14:44:35 +00:00
parent 2a3cf8ac58
commit 4050866b3b
5 changed files with 287 additions and 101 deletions

View File

@ -14,63 +14,88 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "history/history_item_components.h"
#include "history/history_message.h"
#include "history/history.h"
#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, Context &&context)
: _data(std::move(data))
, _context(std::move(context))
, _reactions(st::msgMinWidth / 2) {
BottomInfo::BottomInfo(Data &&data)
: _data(std::move(data)) {
layout();
}
void BottomInfo::update(Data &&data, Context &&context, int availableWidth) {
void BottomInfo::update(Data &&data, int availableWidth) {
_data = std::move(data);
_context = std::move(context);
layout();
if (width() > 0) {
resizeGetHeight(std::min(maxWidth(), availableWidth));
}
}
int BottomInfo::countReactionsMaxWidth() const {
auto result = 0;
for (const auto &reaction : _reactions) {
result += st::reactionInfoSize;
if (reaction.countTextWidth > 0) {
result += st::reactionInfoSkip
+ reaction.countTextWidth
+ st::reactionInfoDigitSkip;
} else {
result += st::reactionInfoBetween;
}
}
if (result) {
result += (st::reactionInfoSkip - st::reactionInfoBetween);
}
return result;
}
int BottomInfo::countReactionsHeight(int newWidth) const {
const auto left = 0;
auto x = 0;
auto y = 0;
auto widthLeft = newWidth;
for (const auto &reaction : _reactions) {
const auto add = (reaction.countTextWidth > 0)
? st::reactionInfoDigitSkip
: st::reactionInfoBetween;
const auto width = st::reactionInfoSize
+ (reaction.countTextWidth > 0
? (st::reactionInfoSkip + reaction.countTextWidth)
: 0);
if (x > left && widthLeft < width) {
x = left;
y += st::msgDateFont->height;
widthLeft = newWidth;
}
x += width + add;
widthLeft -= width + add;
}
if (x > left) {
y += st::msgDateFont->height;
}
return y;
}
int BottomInfo::firstLineWidth() const {
if (height() == minHeight()) {
return width();
}
const auto reactionsWidth = _reactions.maxWidth();
const auto noReactionsWidth = maxWidth()
- st::historyReactionsSkip
- reactionsWidth;
return noReactionsWidth;
return maxWidth() - _reactionsMaxWidth;
}
TextState BottomInfo::textState(
not_null<const HistoryItem*> item,
QPoint position) const {
auto result = TextState(item);
if (!_reactions.isEmpty()) {
const auto reactionsPosition = [&] {
if (height() == minHeight()) {
return QPoint(0, 0);
}
const auto available = width();
const auto use = std::min(available, _reactions.maxWidth());
return QPoint(width() - use, st::msgDateFont->height);
}();
const auto state = _reactions.getStateLeft(
position - reactionsPosition,
std::min(width(), _reactions.maxWidth()),
width());
if (state.uponSymbol) {
result.link = _context.reactions;
return result;
}
}
const auto inTime = QRect(
width() - _dateWidth,
0,
@ -168,24 +193,54 @@ void BottomInfo::paint(
firstLineBottom + st::historyViewsTop,
outerWidth);
}
if (!_reactions.isEmpty()) {
if (height() == minHeight()) {
_reactions.drawLeft(
p,
position.x(),
position.y(),
_reactions.maxWidth(),
outerWidth);
} else {
const auto available = width();
const auto use = std::min(available, _reactions.maxWidth());
_reactions.drawLeft(
p,
position.x() + width() - use,
position.y() + st::msgDateFont->height,
use,
outerWidth);
if (!_reactions.empty()) {
auto left = position.x();
auto top = position.y();
auto available = width();
if (height() != minHeight()) {
available = std::min(available, _reactionsMaxWidth);
left += width() - available;
top += st::msgDateFont->height;
}
paintReactions(p, left, top, available);
}
}
void BottomInfo::paintReactions(
Painter &p,
int left,
int top,
int availableWidth) const {
auto x = left;
auto y = top;
auto widthLeft = availableWidth;
for (const auto &reaction : _reactions) {
const auto add = (reaction.countTextWidth > 0)
? st::reactionInfoDigitSkip
: st::reactionInfoBetween;
const auto width = st::reactionInfoSize
+ (reaction.countTextWidth > 0
? (st::reactionInfoSkip + reaction.countTextWidth)
: 0);
if (x > left && widthLeft < width) {
x = left;
y += st::msgDateFont->height;
widthLeft = availableWidth;
}
if (!reaction.image.isNull()) {
p.drawImage(
x,
y + (st::msgDateFont->height - st::reactionInfoSize) / 2,
reaction.image);
}
if (reaction.countTextWidth > 0) {
p.drawText(
x + st::reactionInfoSize + st::reactionInfoSkip,
y + st::msgDateFont->ascent,
reaction.countText);
}
x += width + add;
widthLeft -= width + add;
}
}
@ -193,14 +248,11 @@ QSize BottomInfo::countCurrentSize(int newWidth) {
if (newWidth >= maxWidth()) {
return optimalSize();
}
const auto reactionsWidth = _reactions.maxWidth();
const auto noReactionsWidth = maxWidth()
- st::historyReactionsSkip
- reactionsWidth;
accumulate_min(newWidth, std::max(noReactionsWidth, reactionsWidth));
const auto noReactionsWidth = maxWidth() - _reactionsMaxWidth;
accumulate_min(newWidth, std::max(noReactionsWidth, _reactionsMaxWidth));
return QSize(
newWidth,
st::msgDateFont->height + _reactions.countHeight(newWidth));
st::msgDateFont->height + countReactionsHeight(newWidth));
}
void BottomInfo::layout() {
@ -274,24 +326,16 @@ void BottomInfo::layoutReactionsText() {
}) | ranges::to_vector;
ranges::sort(sorted, std::greater<>(), &std::pair<QString, int>::second);
auto text = TextWithEntities();
for (const auto &[string, count] : sorted) {
if (!text.text.isEmpty()) {
text.append(" - ");
}
const auto chosen = (_data.chosenReaction == string);
text.append(string);
if (_data.chosenReaction == string) {
text.append(Ui::Text::Bold(QString::number(count)));
} else {
text.append(QString::number(count));
}
auto reactions = std::vector<Reaction>();
reactions.reserve(sorted.size());
for (const auto &[emoji, count] : sorted) {
const auto i = ranges::find(_reactions, emoji, &Reaction::emoji);
reactions.push_back((i != end(_reactions))
? std::move(*i)
: prepareReactionWithEmoji(emoji));
setReactionCount(reactions.back(), count);
}
_reactions.setMarkedText(
st::msgDateTextStyle,
text,
Ui::NameTextOptions());
_reactions = std::move(reactions);
}
QSize BottomInfo::countOptimalSize() {
@ -310,16 +354,137 @@ QSize BottomInfo::countOptimalSize() {
+ _replies.maxWidth()
+ st::historyViewsWidth;
}
if (!_reactions.isEmpty()) {
width += st::historyReactionsSkip + _reactions.maxWidth();
}
_reactionsMaxWidth = countReactionsMaxWidth();
width += _reactionsMaxWidth;
return QSize(width, st::msgDateFont->height);
}
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);
}
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;
}
reaction.count = count;
reaction.countText = (count > 1)
? Lang::FormatCountToShort(count).string
: QString();
reaction.countTextWidth = (count > 1)
? st::msgDateFont->width(reaction.countText)
: 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;
auto result = BottomInfo::Data();
const auto owner = &message->data()->history()->owner();
auto result = BottomInfo::Data{ .owner = owner };
const auto item = message->message();
result.date = message->dateTime();
@ -364,13 +529,4 @@ BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
return result;
}
BottomInfo::Context BottomInfoContextFromMessage(
not_null<Message*> message) {
auto result = BottomInfo::Context();
result.reactions = std::make_shared<LambdaClickHandler>([=] {
message->delegate()->elementShowReactions(message);
});
return result;
}
} // namespace HistoryView

View File

@ -15,6 +15,11 @@ namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace Data {
class Session;
class DocumentMedia;
} // namespace Data
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
@ -36,6 +41,7 @@ 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;
@ -44,12 +50,9 @@ public:
std::optional<int> replies;
Flags flags;
};
struct Context {
ClickHandlerPtr reactions;
};
BottomInfo(Data &&data, Context &&context);
explicit BottomInfo(Data &&data);
void update(Data &&data, Context &&context, int availableWidth);
void update(Data &&data, int availableWidth);
[[nodiscard]] int firstLineWidth() const;
[[nodiscard]] TextState textState(
@ -66,22 +69,53 @@ public:
const PaintContext &context) const;
private:
struct Reaction {
QImage image;
QString emoji;
std::shared_ptr<::Data::DocumentMedia> media;
QString countText;
int count = 0;
int countTextWidth = 0;
};
void layout();
void layoutDateText();
void layoutViewsText();
void layoutRepliesText();
void layoutReactionsText();
[[nodiscard]] int countReactionsMaxWidth() const;
[[nodiscard]] int countReactionsHeight(int newWidth) const;
void paintReactions(
Painter &p,
int left,
int top,
int availableWidth) const;
QSize countOptimalSize() override;
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;
Data _data;
Context _context;
Ui::Text::String _authorEditedDate;
Ui::Text::String _views;
Ui::Text::String _replies;
Ui::Text::String _reactions;
std::vector<Reaction> _reactions;
int _reactionsMaxWidth = 0;
int _dateWidth = 0;
rpl::lifetime _assetsLoadLifetime;
bool _waitingForReactionsList = false;
bool _waitingForDownloadTask = false;
bool _authorElided = false;
};
@ -89,7 +123,4 @@ private:
[[nodiscard]] BottomInfo::Data BottomInfoDataFromMessage(
not_null<Message*> message);
[[nodiscard]] BottomInfo::Context BottomInfoContextFromMessage(
not_null<Message*> message);
} // namespace HistoryView

View File

@ -245,9 +245,7 @@ Message::Message(
not_null<HistoryMessage*> data,
Element *replacing)
: Element(delegate, data, replacing)
, _bottomInfo(
BottomInfoDataFromMessage(this),
BottomInfoContextFromMessage(this)) {
, _bottomInfo(BottomInfoDataFromMessage(this)) {
initLogEntryOriginal();
initPsa();
refreshReactions();
@ -1938,10 +1936,7 @@ void Message::itemDataChanged() {
? _reactions->currentSize()
: QSize();
refreshReactions();
_bottomInfo.update(
BottomInfoDataFromMessage(this),
BottomInfoContextFromMessage(this),
width());
_bottomInfo.update(BottomInfoDataFromMessage(this), width());
const auto nowInfo = _bottomInfo.currentSize();
const auto nowReactions = _reactions
? _reactions->currentSize()

View File

@ -69,6 +69,7 @@ private:
int count = 0;
int countTextWidth = 0;
};
void layout();
void layoutButtons();

View File

@ -210,8 +210,6 @@ historyPinOutIcon: icon {{ "history_pin", historyOutIconFg }};
historyPinOutSelectedIcon: icon {{ "history_pin", historyOutIconFgSelected }};
historyPinInvertedIcon: icon {{ "history_pin", historySendingInvertedIconFg }};
historyReactionsSkip: 8px;
historyComposeField: InputField(defaultInputField) {
font: msgFont;
textMargins: margins(0px, 0px, 0px, 0px);
@ -961,6 +959,11 @@ reactionBottomSkip: 3px;
reactionBottomBetween: 4px;
reactionBottomInBubbleLeft: -3px;
reactionInfoSize: 15px;
reactionInfoSkip: 3px;
reactionInfoDigitSkip: 6px;
reactionInfoBetween: 3px;
reactionCornerSize: size(27px, 19px);
reactionCornerCenter: point(-6px, -5px);
reactionCornerImage: 15px;