From 9b43d204e2c8fe2e471edb73ba5a9986eb4bd174 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 3 Jan 2024 14:46:50 +0400 Subject: [PATCH] Track and render reactions as tags in Saved Messages. --- .../SourceFiles/data/data_saved_messages.cpp | 4 +- Telegram/SourceFiles/data/data_types.h | 2 + Telegram/SourceFiles/history/history_item.cpp | 57 +++--- Telegram/SourceFiles/history/history_item.h | 1 + .../history/view/history_view_message.cpp | 6 +- .../view/reactions/history_view_reactions.cpp | 166 ++++++++++++++++-- .../view/reactions/history_view_reactions.h | 11 ++ Telegram/SourceFiles/ui/chat/chat.style | 6 + Telegram/SourceFiles/ui/empty_userpic.cpp | 2 +- 9 files changed, 216 insertions(+), 39 deletions(-) diff --git a/Telegram/SourceFiles/data/data_saved_messages.cpp b/Telegram/SourceFiles/data/data_saved_messages.cpp index 66bc87609..d81f97fe5 100644 --- a/Telegram/SourceFiles/data/data_saved_messages.cpp +++ b/Telegram/SourceFiles/data/data_saved_messages.cpp @@ -20,6 +20,8 @@ namespace { constexpr auto kPerPage = 50; constexpr auto kFirstPerPage = 10; +constexpr auto kListPerPage = 100; +constexpr auto kListFirstPerPage = 20; } // namespace @@ -82,7 +84,7 @@ void SavedMessages::sendLoadMore() { MTP_int(_offsetDate), MTP_int(_offsetId), _offsetPeer ? _offsetPeer->input : MTP_inputPeerEmpty(), - MTP_int(kPerPage), + MTP_int(_offsetId ? kListPerPage : kListFirstPerPage), MTP_long(0)) // hash ).done([=](const MTPmessages_SavedDialogs &result) { apply(result, false); diff --git a/Telegram/SourceFiles/data/data_types.h b/Telegram/SourceFiles/data/data_types.h index 31c976859..46f75419a 100644 --- a/Telegram/SourceFiles/data/data_types.h +++ b/Telegram/SourceFiles/data/data_types.h @@ -313,6 +313,8 @@ enum class MessageFlag : uint64 { ShowSimilarChannels = (1ULL << 41), Sponsored = (1ULL << 42), + + ReactionsAreTags = (1ULL << 43), }; inline constexpr bool is_flag_type(MessageFlag) { return true; } using MessageFlags = base::flags; diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index dcdd8b3d0..35a2dc7db 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -2429,6 +2429,10 @@ const std::vector &HistoryItem::reactions() const { return _reactions ? _reactions->list() : kEmpty; } +bool HistoryItem::reactionsAreTags() const { + return _flags & MessageFlag::ReactionsAreTags; +} + auto HistoryItem::recentReactions() const -> const base::flat_map< Data::ReactionId, @@ -3556,31 +3560,40 @@ bool HistoryItem::changeReactions(const MTPMessageReactions *reactions) { } if (!reactions) { _flags &= ~MessageFlag::CanViewReactions; + if (_history->peer->isSelf()) { + _flags |= MessageFlag::ReactionsAreTags; + } return (base::take(_reactions) != nullptr); } - return reactions->match([&](const MTPDmessageReactions &data) { - if (data.is_can_see_list()) { - _flags |= MessageFlag::CanViewReactions; - } else { - _flags &= ~MessageFlag::CanViewReactions; + const auto &data = reactions->data(); + const auto empty = data.vresults().v.isEmpty(); + if (data.is_reactions_as_tags() + || (empty && _history->peer->isSelf())) { + _flags |= MessageFlag::ReactionsAreTags; + } else { + _flags &= ~MessageFlag::ReactionsAreTags; + } + if (data.is_can_see_list()) { + _flags |= MessageFlag::CanViewReactions; + } else { + _flags &= ~MessageFlag::CanViewReactions; + } + if (empty) { + return (base::take(_reactions) != nullptr); + } else if (!_reactions) { + _reactions = std::make_unique(this); + } + const auto min = data.is_min(); + const auto &list = data.vresults().v; + const auto &recent = data.vrecent_reactions().value_or_empty(); + if (min && hasUnreadReaction()) { + // We can't update reactions from min if we have unread. + if (_reactions->checkIfChanged(list, recent, min)) { + updateReactionsUnknown(); } - if (data.vresults().v.isEmpty()) { - return (base::take(_reactions) != nullptr); - } else if (!_reactions) { - _reactions = std::make_unique(this); - } - const auto min = data.is_min(); - const auto &list = data.vresults().v; - const auto &recent = data.vrecent_reactions().value_or_empty(); - if (min && hasUnreadReaction()) { - // We can't update reactions from min if we have unread. - if (_reactions->checkIfChanged(list, recent, min)) { - updateReactionsUnknown(); - } - return false; - } - return _reactions->change(list, recent, min); - }); + return false; + } + return _reactions->change(list, recent, min); } void HistoryItem::applyTTL(const MTPDmessage &data) { diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 412b9e057..75705eab8 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -454,6 +454,7 @@ public: not_null from) const; [[nodiscard]] crl::time lastReactionsRefreshTime() const; + [[nodiscard]] bool reactionsAreTags() const; [[nodiscard]] bool hasDirectLink() const; [[nodiscard]] bool changesWallPaper() const; diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 4edffbe37..f319dfc9a 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -2917,8 +2917,12 @@ bool Message::isSignedAuthorElided() const { bool Message::embedReactionsInBottomInfo() const { const auto item = data(); const auto user = item->history()->peer->asUser(); - if (!user || user->isPremium() || user->session().premium()) { + if (!user + || user->isPremium() + || user->isSelf() + || user->session().premium()) { // Only in messages of a non premium user with a non premium user. + // In saved messages we use reactions for tags, we don't embed them. return false; } auto seenMy = false; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp index 911472560..a7e83c029 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.cpp @@ -55,6 +55,7 @@ struct InlineList::Button { int count = 0; int countTextWidth = 0; bool chosen = false; + bool tag = false; }; InlineList::InlineList( @@ -118,6 +119,7 @@ void InlineList::layoutButtons() { ) | ranges::views::transform([](const MessageReaction &reaction) { return not_null{ &reaction }; }) | ranges::to_vector; + const auto tags = _data.flags & Data::Flag::Tags; const auto &list = _owner->list(::Data::Reactions::Type::All); ranges::sort(sorted, [&]( not_null a, @@ -142,8 +144,10 @@ void InlineList::layoutButtons() { buttons.push_back((i != end(_buttons)) ? std::move(*i) : prepareButtonWithId(id)); - const auto j = _data.recent.find(id); - if (j != end(_data.recent) && !j->second.empty()) { + if (tags) { + setButtonTag(buttons.back()); + } else if (const auto j = _data.recent.find(id) + ; j != end(_data.recent) && !j->second.empty()) { setButtonUserpics(buttons.back(), j->second); } else { setButtonCount(buttons.back(), reaction->count); @@ -168,12 +172,22 @@ InlineList::Button InlineList::prepareButtonWithId(const ReactionId &id) { return result; } +void InlineList::setButtonTag(Button &button) { + if (button.tag) { + return; + } + button.userpics = nullptr; + button.count = 0; + button.tag = true; +} + void InlineList::setButtonCount(Button &button, int count) { - if (button.count == count && !button.userpics) { + if (!button.tag && button.count == count && !button.userpics) { return; } button.userpics = nullptr; button.count = count; + button.tag = false; button.countText = Lang::FormatCountToShort(count).string; button.countTextWidth = st::semiboldFont->width(button.countText); } @@ -181,6 +195,7 @@ void InlineList::setButtonCount(Button &button, int count) { void InlineList::setButtonUserpics( Button &button, const std::vector> &peers) { + button.tag = false; if (!button.userpics) { button.userpics = std::make_unique(); } @@ -228,6 +243,10 @@ QSize InlineList::countOptimalSize() { const auto between = st::reactionInlineBetween; const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; + const auto widthBaseTag = padding.left() + + size + + st::reactionInlineTagSkip + + padding.right(); const auto widthBaseCount = padding.left() + size + st::reactionInlineSkip @@ -245,7 +264,9 @@ QSize InlineList::countOptimalSize() { }; const auto height = padding.top() + size + padding.bottom(); for (auto &button : _buttons) { - const auto width = button.userpics + const auto width = button.tag + ? widthBaseTag + : button.userpics ? (widthBaseUserpics + userpicsWidth(button)) : (widthBaseCount + button.countTextWidth); button.geometry.setSize({ width, height }); @@ -336,7 +357,8 @@ void InlineList::paint( const auto padding = st::reactionInlinePadding; const auto size = st::reactionInlineSize; const auto skip = (size - st::reactionInlineImage) / 2; - const auto inbubble = (_data.flags & InlineListData::Flag::InBubble); + const auto tags = (_data.flags & Data::Flag::Tags); + const auto inbubble = (_data.flags & Data::Flag::InBubble); const auto flipped = (_data.flags & Data::Flag::Flipped); p.setFont(st::semiboldFont); for (const auto &button : _buttons) { @@ -366,21 +388,26 @@ void InlineList::paint( if (bubbleProgress > 0.) { auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); + auto opacity = 1.; + auto color = QColor(); if (inbubble) { if (!chosen) { - p.setOpacity(bubbleProgress * (context.outbg + opacity = bubbleProgress * (context.outbg ? kOutNonChosenOpacity - : kInNonChosenOpacity)); + : kInNonChosenOpacity); } else if (!bubbleReady) { - p.setOpacity(bubbleProgress); + opacity = bubbleProgress; } - p.setBrush(stm->msgFileBg); + color = stm->msgFileBg->c; } else { if (!bubbleReady) { - p.setOpacity(bubbleProgress); + opacity = bubbleProgress; } - p.setBrush(chosen ? st->msgServiceFg() : st->msgServiceBg()); + color = (chosen + ? st->msgServiceFg() + : st->msgServiceBg())->c; } + const auto radius = geometry.height() / 2.; const auto fill = geometry.marginsAdded({ flipped ? bubbleSkip : 0, @@ -388,7 +415,7 @@ void InlineList::paint( flipped ? 0 : bubbleSkip, 0, }); - p.drawRoundedRect(fill, radius, radius); + paintSingleBg(p, fill, color, opacity); if (inbubble && !chosen) { p.setOpacity(bubbleProgress); } @@ -434,7 +461,8 @@ void InlineList::paint( .target = image, }); } - if (bubbleProgress == 0.) { + if (tags || bubbleProgress == 0.) { + p.setOpacity(1.); continue; } resolveUserpicsImage(button); @@ -479,6 +507,115 @@ void InlineList::paint( } } +void InlineList::validateTagBg(const QColor &color) const { + if (!_tagBg.isNull() && _tagBgColor == color) { + return; + } + _tagBgColor = color; + + const auto padding = st::reactionInlinePadding; + const auto size = st::reactionInlineSize; + const auto width = padding.left() + + size + + st::reactionInlineTagSkip + + padding.right(); + const auto height = padding.top() + size + padding.bottom(); + const auto ratio = style::DevicePixelRatio(); + + auto mask = QImage( + QSize(width, height) * ratio, + QImage::Format_ARGB32_Premultiplied); + mask.setDevicePixelRatio(ratio); + + mask.fill(Qt::transparent); + auto p = QPainter(&mask); + + auto path = QPainterPath(); + const auto arrow = st::reactionInlineTagArrow; + const auto rradius = st::reactionInlineTagRightRadius * 1.; + const auto radius = st::reactionInlineTagLeftRadius - rradius; + const auto fg = QColor(255, 255, 255); + auto pen = QPen(fg); + pen.setWidthF(rradius * 2.); + pen.setJoinStyle(Qt::RoundJoin); + const auto rect = QRectF(0, 0, width, height).marginsRemoved( + { rradius, rradius, rradius, rradius }); + + const auto right = rect.x() + rect.width(); + const auto bottom = rect.y() + rect.height(); + path.moveTo(rect.x() + radius, rect.y()); + path.lineTo(right - arrow, rect.y()); + path.lineTo(right, rect.y() + rect.height() / 2); + path.lineTo(right - arrow, bottom); + path.lineTo(rect.x() + radius, bottom); + path.arcTo(QRectF(rect.x(), bottom - radius * 2, radius * 2, radius * 2), 270, -90); + path.lineTo(rect.x(), rect.y() + radius); + path.arcTo(QRectF(rect.x(), rect.y(), radius * 2, radius * 2), 180, -90); + path.closeSubpath(); + + const auto dsize = st::reactionInlineTagDot; + const auto dot = QRectF( + right - st::reactionInlineTagDotSkip - dsize, + rect.y() + (rect.height() - dsize) / 2., + dsize, + dsize); + + auto hq = PainterHighQualityEnabler(p); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setPen(pen); + p.setBrush(fg); + p.drawPath(path); + + p.setPen(Qt::NoPen); + p.setBrush(QColor(255, 255, 255, 255 * 0.6)); + p.drawEllipse(dot); + + p.end(); + + _tagBg = style::colorizeImage(mask, color); +} + +void InlineList::paintSingleBg( + Painter &p, + const QRect &fill, + const QColor &color, + float64 opacity) const { + p.setOpacity(opacity); + if (!(_data.flags & Data::Flag::Tags)) { + const auto radius = fill.height() / 2.; + p.setBrush(color); + p.drawRoundedRect(fill, radius, radius); + return; + } + validateTagBg(color); + const auto ratio = style::DevicePixelRatio(); + const auto left = st::reactionInlineTagLeftRadius; + const auto right = (_tagBg.width() / ratio) - left; + Assert(right > 0); + const auto useLeft = std::min(fill.width(), left); + p.drawImage( + QRect(fill.x(), fill.y(), useLeft, fill.height()), + _tagBg, + QRect(0, 0, useLeft * ratio, _tagBg.height())); + const auto middle = fill.width() - left - right; + if (middle > 0) { + p.fillRect(fill.x() + left, fill.y(), middle, fill.height(), color); + } + if (const auto useRight = fill.width() - left; useRight > 0) { + p.drawImage( + QRect( + fill.x() + fill.width() - useRight, + fill.y(), + useRight, + fill.height()), + _tagBg, + QRect(_tagBg.width() - useRight * ratio, + 0, + useRight * ratio, + _tagBg.height())); + } +} + bool InlineList::getState( QPoint point, not_null outResult) const { @@ -654,7 +791,8 @@ InlineListData InlineListDataFromMessage(not_null message) { } } result.flags = (message->hasOutLayout() ? Flag::OutLayout : Flag()) - | (message->embedReactionsInBubble() ? Flag::InBubble : Flag()); + | (message->embedReactionsInBubble() ? Flag::InBubble : Flag()) + | (item->reactionsAreTags() ? Flag::Tags : Flag()); return result; } diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h index 398ec0cd8..cc4fa80d8 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions.h @@ -41,6 +41,7 @@ struct InlineListData { InBubble = 0x01, OutLayout = 0x02, Flipped = 0x04, + Tags = 0x08, }; friend inline constexpr bool is_flag_type(Flag) { return true; }; using Flags = base::flags; @@ -103,6 +104,7 @@ private: void layout(); void layoutButtons(); + void setButtonTag(Button &button); void setButtonCount(Button &button, int count); void setButtonUserpics( Button &button, @@ -115,6 +117,13 @@ private: QPoint innerTopLeft, const PaintContext &context, const QColor &textColor) const; + void paintSingleBg( + Painter &p, + const QRect &fill, + const QColor &color, + float64 opacity) const; + + void validateTagBg(const QColor &color) const; QSize countOptimalSize() override; @@ -124,6 +133,8 @@ private: Data _data; std::vector