tdesktop/Telegram/SourceFiles/history/view/history_view_bottom_info.cpp

646 lines
17 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/history_view_bottom_info.h"
#include "ui/chat/message_bubble.h"
#include "ui/chat/chat_style.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/text/text_block.h"
#include "lang/lang_keys.h"
#include "history/history_item_components.h"
#include "history/history_message.h"
#include "history/history.h"
#include "history/view/reactions/history_view_reactions_animation.h"
#include "history/view/history_view_message.h"
#include "history/view/history_view_cursor_state.h"
#include "core/click_handler_types.h"
#include "main/main_session.h"
#include "lottie/lottie_icon.h"
#include "data/data_session.h"
#include "data/data_message_reactions.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_dialogs.h"
namespace HistoryView {
ReactionAnimationArgs ReactionAnimationArgs::translated(
QPoint point) const {
return {
.id = id,
.flyIcon = flyIcon,
.flyFrom = flyFrom.translated(point),
};
}
BottomInfo::BottomInfo(
not_null<::Data::Reactions*> reactionsOwner,
Data &&data)
: _reactionsOwner(reactionsOwner)
, _data(std::move(data)) {
layout();
}
BottomInfo::~BottomInfo() = default;
void BottomInfo::update(Data &&data, int availableWidth) {
_data = std::move(data);
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();
}
return maxWidth() - _reactionsMaxWidth;
}
bool BottomInfo::isWide() const {
return (_data.flags & Data::Flag::Edited)
|| !_data.author.isEmpty()
|| !_views.isEmpty()
|| !_replies.isEmpty()
|| !_reactions.empty();
}
TextState BottomInfo::textState(
not_null<const HistoryItem*> item,
QPoint position) const {
auto result = TextState(item);
if (const auto link = revokeReactionLink(item, position)) {
result.link = link;
return result;
}
const auto textWidth = _authorEditedDate.maxWidth();
auto withTicksWidth = textWidth;
if (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) {
withTicksWidth += st::historySendStateSpace;
}
const auto inTime = QRect(
width() - withTicksWidth,
0,
withTicksWidth,
st::msgDateFont->height
).contains(position);
if (inTime) {
result.cursor = CursorState::Date;
}
return result;
}
ClickHandlerPtr BottomInfo::revokeReactionLink(
not_null<const HistoryItem*> item,
QPoint position) const {
if (_reactions.empty()) {
return nullptr;
}
auto left = 0;
auto top = 0;
auto available = width();
if (height() != minHeight()) {
available = std::min(available, _reactionsMaxWidth);
left += width() - available;
top += st::msgDateFont->height;
}
auto x = left;
auto y = top;
auto widthLeft = available;
for (const auto &reaction : _reactions) {
const auto chosen = reaction.chosen;
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 = available;
}
const auto image = QRect(
x,
y,
st::reactionInfoSize,
st::msgDateFont->height);
if (chosen && image.contains(position)) {
if (!_revokeLink) {
_revokeLink = revokeReactionLink(item);
}
return _revokeLink;
}
x += width + add;
widthLeft -= width + add;
}
return nullptr;
}
ClickHandlerPtr BottomInfo::revokeReactionLink(
not_null<const HistoryItem*> item) const {
const auto itemId = item->fullId();
const auto sessionId = item->history()->session().uniqueId();
return std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
if (controller->session().uniqueId() == sessionId) {
auto &owner = controller->session().data();
if (const auto item = owner.message(itemId)) {
const auto chosen = item->chosenReactions();
if (!chosen.empty()) {
item->toggleReaction(
chosen.front(),
HistoryItem::ReactionSource::Existing);
}
}
}
}
});
}
bool BottomInfo::isSignedAuthorElided() const {
return _authorElided;
}
void BottomInfo::paint(
Painter &p,
QPoint position,
int outerWidth,
bool unread,
bool inverted,
const PaintContext &context) const {
const auto st = context.st;
const auto stm = context.messageStyle();
auto right = position.x() + width();
const auto firstLineBottom = position.y() + st::msgDateFont->height;
if (_data.flags & Data::Flag::OutLayout) {
const auto &icon = (_data.flags & Data::Flag::Sending)
? (inverted
? st->historySendingInvertedIcon()
: st->historySendingIcon())
: unread
? (inverted
? st->historySentInvertedIcon()
: stm->historySentIcon)
: (inverted
? st->historyReceivedInvertedIcon()
: stm->historyReceivedIcon);
icon.paint(
p,
QPoint(right, firstLineBottom) + st::historySendStatePosition,
outerWidth);
right -= st::historySendStateSpace;
}
const auto authorEditedWidth = _authorEditedDate.maxWidth();
right -= authorEditedWidth;
_authorEditedDate.drawLeft(
p,
right,
position.y(),
authorEditedWidth,
outerWidth);
if (_data.flags & Data::Flag::Pinned) {
const auto &icon = inverted
? st->historyPinInvertedIcon()
: stm->historyPinIcon;
right -= st::historyPinWidth;
icon.paint(
p,
right,
firstLineBottom + st::historyPinTop,
outerWidth);
}
if (!_views.isEmpty()) {
const auto viewsWidth = _views.maxWidth();
right -= st::historyViewsSpace + viewsWidth;
_views.drawLeft(p, right, position.y(), viewsWidth, outerWidth);
const auto &icon = inverted
? st->historyViewsInvertedIcon()
: stm->historyViewsIcon;
right -= st::historyViewsWidth;
icon.paint(
p,
right,
firstLineBottom + st::historyViewsTop,
outerWidth);
}
if (!_replies.isEmpty()) {
const auto repliesWidth = _replies.maxWidth();
right -= st::historyViewsSpace + repliesWidth;
_replies.drawLeft(p, right, position.y(), repliesWidth, outerWidth);
const auto &icon = inverted
? st->historyRepliesInvertedIcon()
: stm->historyRepliesIcon;
right -= st::historyViewsWidth;
icon.paint(
p,
right,
firstLineBottom + st::historyViewsTop,
outerWidth);
}
if ((_data.flags & Data::Flag::Sending)
&& !(_data.flags & Data::Flag::OutLayout)) {
right -= st::historySendStateSpace;
const auto &icon = inverted
? st->historyViewsSendingInvertedIcon()
: st->historyViewsSendingIcon();
icon.paint(
p,
right,
firstLineBottom + st::historyViewsTop,
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, position, left, top, available, context);
}
}
void BottomInfo::paintReactions(
Painter &p,
QPoint origin,
int left,
int top,
int availableWidth,
const PaintContext &context) const {
struct SingleAnimation {
not_null<Reactions::Animation*> animation;
QRect target;
};
std::vector<SingleAnimation> animations;
auto x = left;
auto y = top;
auto widthLeft = availableWidth;
for (const auto &reaction : _reactions) {
if (context.reactionInfo
&& reaction.animation
&& reaction.animation->finished()) {
reaction.animation = nullptr;
}
const auto animating = (reaction.animation != nullptr);
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()) {
reaction.image = _reactionsOwner->resolveImageFor(
reaction.id,
::Data::Reactions::ImageSize::BottomInfo);
}
const auto image = QRect(
x + (st::reactionInfoSize - st::reactionInfoImage) / 2,
y + (st::msgDateFont->height - st::reactionInfoImage) / 2,
st::reactionInfoImage,
st::reactionInfoImage);
const auto skipImage = animating
&& (reaction.count < 2 || !reaction.animation->flying());
if (!reaction.image.isNull() && !skipImage) {
p.drawImage(image.topLeft(), reaction.image);
}
if (animating) {
animations.push_back({
.animation = reaction.animation.get(),
.target = image,
});
}
if (reaction.countTextWidth > 0) {
p.drawText(
x + st::reactionInfoSize + st::reactionInfoSkip,
y + st::msgDateFont->ascent,
reaction.countText);
}
x += width + add;
widthLeft -= width + add;
}
if (!animations.empty()) {
context.reactionInfo->effectPaint = [=](QPainter &p) {
auto result = QRect();
for (const auto &single : animations) {
const auto area = single.animation->paintGetArea(
p,
origin,
single.target);
result = result.isEmpty() ? area : result.united(area);
}
return result;
};
}
}
QSize BottomInfo::countCurrentSize(int newWidth) {
if (newWidth >= maxWidth()) {
return optimalSize();
}
const auto noReactionsWidth = maxWidth() - _reactionsMaxWidth;
accumulate_min(newWidth, std::max(noReactionsWidth, _reactionsMaxWidth));
return QSize(
newWidth,
st::msgDateFont->height + countReactionsHeight(newWidth));
}
void BottomInfo::layout() {
layoutDateText();
layoutViewsText();
layoutRepliesText();
layoutReactionsText();
initDimensions();
}
void BottomInfo::layoutDateText() {
const auto edited = (_data.flags & Data::Flag::Edited)
? (tr::lng_edited(tr::now) + ' ')
: QString();
const auto author = _data.author;
const auto prefix = !author.isEmpty() ? qsl(", ") : QString();
const auto date = edited + _data.date.toString(cTimeFormat());
const auto afterAuthor = prefix + date;
const auto afterAuthorWidth = st::msgDateFont->width(afterAuthor);
const auto authorWidth = st::msgDateFont->width(author);
const auto maxWidth = st::maxSignatureSize;
_authorElided = !author.isEmpty()
&& (authorWidth + afterAuthorWidth > maxWidth);
const auto name = _authorElided
? st::msgDateFont->elided(author, maxWidth - afterAuthorWidth)
: author;
const auto full = (_data.flags & Data::Flag::Recommended)
? tr::lng_recommended(tr::now)
: (_data.flags & Data::Flag::Sponsored)
? tr::lng_sponsored(tr::now)
: (_data.flags & Data::Flag::Imported)
? (date + ' ' + tr::lng_imported(tr::now))
: name.isEmpty()
? date
: (name + afterAuthor);
_authorEditedDate.setText(
st::msgDateTextStyle,
full,
Ui::NameTextOptions());
}
void BottomInfo::layoutViewsText() {
if (!_data.views || (_data.flags & Data::Flag::Sending)) {
_views.clear();
return;
}
_views.setText(
st::msgDateTextStyle,
Lang::FormatCountToShort(std::max(*_data.views, 1)).string,
Ui::NameTextOptions());
}
void BottomInfo::layoutRepliesText() {
if (!_data.replies
|| !*_data.replies
|| (_data.flags & Data::Flag::RepliesContext)
|| (_data.flags & Data::Flag::Sending)) {
_replies.clear();
return;
}
_replies.setText(
st::msgDateTextStyle,
Lang::FormatCountToShort(*_data.replies).string,
Ui::NameTextOptions());
}
void BottomInfo::layoutReactionsText() {
if (_data.reactions.empty()) {
_reactions.clear();
return;
}
auto sorted = ranges::view::all(
_data.reactions
) | ranges::view::transform([](const MessageReaction &reaction) {
return not_null{ &reaction };
}) | ranges::to_vector;
ranges::sort(
sorted,
std::greater<>(),
&MessageReaction::count);
auto reactions = std::vector<Reaction>();
reactions.reserve(sorted.size());
for (const auto &reaction : sorted) {
const auto &id = reaction->id;
const auto i = ranges::find(_reactions, id, &Reaction::id);
reactions.push_back((i != end(_reactions))
? std::move(*i)
: prepareReactionWithId(id));
setReactionCount(reactions.back(), reaction->count);
}
_reactions = std::move(reactions);
}
QSize BottomInfo::countOptimalSize() {
auto width = 0;
if (_data.flags & (Data::Flag::OutLayout | Data::Flag::Sending)) {
width += st::historySendStateSpace;
}
width += _authorEditedDate.maxWidth();
if (!_views.isEmpty()) {
width += st::historyViewsSpace
+ _views.maxWidth()
+ st::historyViewsWidth;
}
if (!_replies.isEmpty()) {
width += st::historyViewsSpace
+ _replies.maxWidth()
+ st::historyViewsWidth;
}
if (_data.flags & Data::Flag::Pinned) {
width += st::historyPinWidth;
}
_reactionsMaxWidth = countReactionsMaxWidth();
width += _reactionsMaxWidth;
return QSize(width, st::msgDateFont->height);
}
BottomInfo::Reaction BottomInfo::prepareReactionWithId(
const ReactionId &id) {
auto result = Reaction{ .id = id };
_reactionsOwner->preloadImageFor(id);
return result;
}
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::animateReaction(
ReactionAnimationArgs &&args,
Fn<void()> repaint) {
const auto i = ranges::find(_reactions, args.id, &Reaction::id);
if (i == end(_reactions)) {
return;
}
i->animation = std::make_unique<Reactions::Animation>(
_reactionsOwner,
args.translated(QPoint(width(), height())),
std::move(repaint),
st::reactionInfoImage);
}
auto BottomInfo::takeReactionAnimations()
-> base::flat_map<ReactionId, std::unique_ptr<Reactions::Animation>> {
auto result = base::flat_map<
ReactionId,
std::unique_ptr<Reactions::Animation>>();
for (auto &reaction : _reactions) {
if (reaction.animation) {
result.emplace(reaction.id, std::move(reaction.animation));
}
}
return result;
}
void BottomInfo::continueReactionAnimations(base::flat_map<
ReactionId,
std::unique_ptr<Reactions::Animation>> animations) {
for (auto &[id, animation] : animations) {
const auto i = ranges::find(_reactions, id, &Reaction::id);
if (i != end(_reactions)) {
i->animation = std::move(animation);
}
}
}
BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {
using Flag = BottomInfo::Data::Flag;
const auto item = message->message();
auto result = BottomInfo::Data();
result.date = message->dateTime();
if (message->embedReactionsInBottomInfo()) {
result.reactions = item->reactions();
}
if (message->hasOutLayout()) {
result.flags |= Flag::OutLayout;
}
if (message->context() == Context::Replies) {
result.flags |= Flag::RepliesContext;
}
if (const auto sponsored = item->Get<HistoryMessageSponsored>()) {
if (sponsored->recommended) {
result.flags |= Flag::Recommended;
}
result.flags |= Flag::Sponsored;
}
if (item->isPinned() && message->context() != Context::Pinned) {
result.flags |= Flag::Pinned;
}
if (const auto msgsigned = item->Get<HistoryMessageSigned>()) {
if (!msgsigned->isAnonymousRank) {
result.author = msgsigned->author;
}
}
if (message->displayedEditDate()) {
result.flags |= Flag::Edited;
}
if (const auto views = item->Get<HistoryMessageViews>()) {
if (views->views.count >= 0) {
result.views = views->views.count;
}
if (views->replies.count >= 0 && !views->commentsMegagroupId) {
result.replies = views->replies.count;
}
}
if (item->isSending() || item->hasFailed()) {
result.flags |= Flag::Sending;
}
const auto forwarded = item->Get<HistoryMessageForwarded>();
if (forwarded && forwarded->imported) {
result.flags |= Flag::Imported;
}
// We don't want to pass and update it in Date for now.
//if (item->unread()) {
// result.flags |= Flag::Unread;
//}
return result;
}
} // namespace HistoryView