Add comments button to channel posts.

This commit is contained in:
John Preston 2020-09-03 11:19:02 +04:00
parent ce91caa820
commit 31e1ed216a
33 changed files with 642 additions and 139 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

View File

@ -1351,6 +1351,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_replies_header#other" = "{count} replies";
"lng_replies_header_none" = "No replies";
"lng_replies_send_placeholder" = "Reply";
"lng_comments_view#one" = "View {count} Comment";
"lng_comments_view#other" = "View {count} Comments";
"lng_comments_view_thread" = "Leave a Comment";
"lng_comments_header#one" = "{count} comment";
"lng_comments_header#other" = "{count} comments";
"lng_comments_header_none" = "No comments";
"lng_comments_open_count#one" = "{count} comment";
"lng_comments_open_count#other" = "{count} comments";
"lng_comments_open_none" = "Leave a comment";
"lng_comments_send_placeholder" = "Comment";
"lng_archived_name" = "Archived chats";
"lng_archived_add" = "Archive";

View File

@ -145,6 +145,9 @@ const ChannelLocation *ChannelData::getLocation() const {
void ChannelData::setLinkedChat(ChannelData *linked) {
if (_linkedChat != linked) {
_linkedChat = linked;
if (const auto history = owner().historyLoaded(this)) {
history->forceFullResize();
}
session().changes().peerUpdated(this, UpdateFlag::ChannelLinkedChat);
}
}

View File

@ -1663,11 +1663,7 @@ bool Session::checkEntitiesAndViewsUpdate(const MTPDmessage &data) {
existing->updateForwardedInfo(data.vfwd_from());
existing->setViewsCount(data.vviews().value_or(-1));
if (const auto replies = data.vreplies()) {
replies->match([&](const MTPDmessageReplies &data) {
existing->setRepliesCount(
data.vreplies().v,
data.vreplies_pts().v);
});
existing->setReplies(*replies);
}
existing->setForwardsCount(data.vforwards().value_or(-1));
if (const auto reply = data.vreply_to()) {

View File

@ -615,6 +615,14 @@ historyPollOutChosenSelected: icon {{ "poll_select_check", historyFileOutIconFgS
historyPollInChosen: icon {{ "poll_select_check", historyFileInIconFg }};
historyPollInChosenSelected: icon {{ "poll_select_check", historyFileInIconFgSelected }};
historyCommentsButtonHeight: 40px;
historyCommentsSkipLeft: 9px;
historyCommentsSkipText: 10px;
historyCommentsUserpicSize: 25px;
historyCommentsUserpicStroke: 2px;
historyCommentsUserpicOverlap: 6px;
historyCommentsSkipRight: 8px;
boxAttachEmoji: IconButton(historyAttachEmoji) {
width: 30px;
height: 30px;
@ -658,6 +666,15 @@ historyQuizTimerInSelected: icon {{ "quiz_timer", msgFileThumbLinkInFgSelected }
historyQuizTimerOut: icon {{ "quiz_timer", msgFileThumbLinkOutFg }};
historyQuizTimerOutSelected: icon {{ "quiz_timer", msgFileThumbLinkOutFgSelected }};
historyCommentsIn: icon {{ "history_comments", msgFileThumbLinkInFg }};
historyCommentsInSelected: icon {{ "history_comments", msgFileThumbLinkInFgSelected }};
historyCommentsOut: icon {{ "history_comments", msgFileThumbLinkOutFg }};
historyCommentsOutSelected: icon {{ "history_comments", msgFileThumbLinkOutFgSelected }};
historyCommentsOpenIn: icon {{ "history_comments_open", msgFileThumbLinkInFg }};
historyCommentsOpenInSelected: icon {{ "history_comments_open", msgFileThumbLinkInFgSelected }};
historyCommentsOpenOut: icon {{ "history_comments_open", msgFileThumbLinkOutFg }};
historyCommentsOpenOutSelected: icon {{ "history_comments_open", msgFileThumbLinkOutFgSelected }};
historySlowmodeCounterMargins: margins(0px, 0px, 10px, 0px);
largeEmojiSize: 36px;

View File

@ -1542,12 +1542,18 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
});
}
if (IsServerMsgId(item->id) && item->repliesCount() > 0) {
_menu->addAction(tr::lng_replies_view(tr::now, lt_count, item->repliesCount()), [=] {
const auto &phrase = item->repliesAreComments()
? tr::lng_comments_view
: tr::lng_replies_view;
_menu->addAction(phrase(tr::now, lt_count, item->repliesCount()), [=] {
controller->showSection(
HistoryView::RepliesMemento(_history, itemId.msg));
});
} else if (const auto replyToTop = item->replyToTop()) {
_menu->addAction(tr::lng_replies_view_thread(tr::now), [=] {
const auto &phrase = item->repliesAreComments()
? tr::lng_comments_view_thread
: tr::lng_replies_view_thread;
_menu->addAction(phrase(tr::now), [=] {
controller->showSection(
HistoryView::RepliesMemento(_history, replyToTop));
});

View File

@ -195,6 +195,9 @@ public:
[[nodiscard]] virtual int repliesCount() const {
return 0;
}
[[nodiscard]] virtual bool repliesAreComments() const {
return false;
}
[[nodiscard]] virtual bool needCheck() const;
@ -252,7 +255,9 @@ public:
}
virtual void setForwardsCount(int count) {
}
virtual void setRepliesCount(int count, int pts) {
virtual void setReplies(const MTPMessageReplies &data) {
}
virtual void changeRepliesCount(int delta, UserId replier) {
}
virtual void setReplyToTop(MsgId replyToTop) {
}

View File

@ -34,10 +34,16 @@ struct HistoryMessageVia : public RuntimeComponent<HistoryMessageVia, HistoryIte
};
struct HistoryMessageViews : public RuntimeComponent<HistoryMessageViews, HistoryItem> {
QString text;
int textWidth = 0;
int views = -1;
int replies = 0;
struct Part {
QString text;
int textWidth = 0;
int count = -1;
};
std::vector<UserId> recentRepliers;
Part views;
Part replies;
ChannelId repliesChannelId = 0;
static constexpr auto kMaxRecentRepliers = 3;
};
struct HistoryMessageSigned : public RuntimeComponent<HistoryMessageSigned, HistoryItem> {

View File

@ -409,7 +409,6 @@ struct HistoryMessage::CreateConfig {
MsgId replyToTop = 0;
UserId viaBotId = 0;
int viewsCount = -1;
int repliesCount = 0;
QString author;
PeerId senderOriginal = 0;
QString senderNameOriginal;
@ -422,6 +421,7 @@ struct HistoryMessage::CreateConfig {
TimeId editDate = 0;
// For messages created from MTP structs.
const MTPMessageReplies *mtpReplies = nullptr;
const MTPReplyMarkup *mtpMarkup = nullptr;
// For messages created from existing messages (forwarded).
@ -474,11 +474,7 @@ HistoryMessage::HistoryMessage(
}
config.viaBotId = data.vvia_bot_id().value_or_empty();
config.viewsCount = data.vviews().value_or(-1);
if (const auto replies = data.vreplies()) {
replies->match([&](const MTPDmessageReplies &data) {
config.repliesCount = data.vreplies().v;
});
}
config.mtpReplies = data.vreplies();
config.mtpMarkup = data.vreply_markup();
config.editDate = data.vedit_date().value_or_empty();
config.author = qs(data.vpost_author().value_or_empty());
@ -744,18 +740,45 @@ void HistoryMessage::createComponentsHelper(
int HistoryMessage::viewsCount() const {
if (const auto views = Get<HistoryMessageViews>()) {
return views->views;
return std::max(views->views.count, 0);
}
return HistoryItem::viewsCount();
}
int HistoryMessage::repliesCount() const {
if (const auto views = Get<HistoryMessageViews>()) {
return views->replies;
if (views->repliesChannelId) {
if (const auto channel = history()->peer->asChannel()) {
const auto linked = channel->linkedChat();
if (!linked || linked->bareId() != views->repliesChannelId) {
return 0;
}
} else {
return 0;
}
}
return std::max(views->replies.count, 0);
}
return HistoryItem::repliesCount();
}
bool HistoryMessage::repliesAreComments() const {
if (const auto views = Get<HistoryMessageViews>()) {
if (!views->repliesChannelId) {
return false;
} else if (const auto channel = history()->peer->asChannel()) {
const auto linked = channel->linkedChat();
if (!linked || linked->bareId() != views->repliesChannelId) {
return false;
}
} else {
return false;
}
return true;
}
return HistoryItem::repliesAreComments();
}
bool HistoryMessage::updateDependencyItem() {
if (const auto reply = Get<HistoryMessageReply>()) {
const auto documentId = reply->replyToDocumentId;
@ -848,7 +871,7 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
if (config.viaBotId) {
mask |= HistoryMessageVia::Bit();
}
if (config.viewsCount >= 0 || config.repliesCount > 0) {
if (config.viewsCount >= 0 || config.mtpReplies) {
mask |= HistoryMessageViews::Bit();
}
if (!config.author.isEmpty()) {
@ -886,8 +909,10 @@ void HistoryMessage::createComponents(const CreateConfig &config) {
via->create(&history()->owner(), config.viaBotId);
}
if (const auto views = Get<HistoryMessageViews>()) {
views->views = config.viewsCount;
views->replies = config.repliesCount;
setViewsCount(config.viewsCount);
if (config.mtpReplies) {
setReplies(*config.mtpReplies);
}
}
if (const auto edited = Get<HistoryMessageEdited>()) {
edited->date = config.editDate;
@ -1400,20 +1425,20 @@ bool HistoryMessage::textHasLinks() const {
void HistoryMessage::setViewsCount(int count) {
const auto views = Get<HistoryMessageViews>();
if (!views
|| views->views == count
|| (count >= 0 && views->views > count)) {
|| views->views.count == count
|| (count >= 0 && views->views.count > count)) {
return;
}
views->views = count;
views->text = (views->views > 0)
? Lang::FormatCountToShort(views->views).string
: QString("1");
const auto was = views->textWidth;
views->textWidth = views->text.isEmpty()
views->views.count = count;
views->views.text = Lang::FormatCountToShort(
std::max(views->views.count, 1)
).string;
const auto was = views->views.textWidth;
views->views.textWidth = views->views.text.isEmpty()
? 0
: st::msgDateFont->width(views->text);
if (was == views->textWidth) {
: st::msgDateFont->width(views->views.text);
if (was == views->views.textWidth) {
history()->owner().requestItemRepaint(this);
} else {
history()->owner().requestItemResize(this);
@ -1423,39 +1448,88 @@ void HistoryMessage::setViewsCount(int count) {
void HistoryMessage::setForwardsCount(int count) {
}
void HistoryMessage::setRepliesCount(int count, int pts) {
auto views = Get<HistoryMessageViews>();
if (!views) {
if (!count) {
void HistoryMessage::setReplies(const MTPMessageReplies &data) {
data.match([&](const MTPDmessageReplies &data) {
auto views = Get<HistoryMessageViews>();
if (!views) {
AddComponents(HistoryMessageViews::Bit());
views = Get<HistoryMessageViews>();
}
const auto repliers = [&] {
auto result = std::vector<UserId>();
if (const auto list = data.vrecent_repliers()) {
result.reserve(list->v.size());
for (const auto &id : list->v) {
result.push_back(id.v);
}
}
return result;
}();
const auto count = data.vreplies().v;
const auto channelId = data.vchannel_id().value_or_empty();
const auto countChanged = (views->replies.count != count);
const auto channelChanged = (views->repliesChannelId != channelId);
const auto recentChanged = (views->recentRepliers != repliers);
if (!countChanged && !channelChanged && !recentChanged) {
return;
}
AddComponents(HistoryMessageViews::Bit());
views = Get<HistoryMessageViews>();
}
if (views->replies == count) {
return;
}
views->replies = count;
if (views->views >= 0) {
return;
} else if (!views->replies) {
RemoveComponents(HistoryMessageViews::Bit());
history()->owner().requestItemResize(this);
return;
}
views->replies.count = count;
if (recentChanged) {
views->recentRepliers = repliers;
}
views->repliesChannelId = channelId;
refreshRepliesText(views, channelChanged);
});
}
views->text = (views->replies > 0)
? Lang::FormatCountToShort(views->replies).string
: QString();
const auto was = views->textWidth;
views->textWidth = views->text.isEmpty()
? 0
: st::msgDateFont->width(views->text);
if (was == views->textWidth) {
history()->owner().requestItemRepaint(this);
void HistoryMessage::refreshRepliesText(
not_null<HistoryMessageViews*> views,
bool forceResize) {
const auto was = views->replies.textWidth;
if (views->repliesChannelId) {
views->replies.text = (views->replies.count > 0)
? tr::lng_comments_open_count(
tr::now,
lt_count_short,
views->replies.count)
: tr::lng_comments_open_none(tr::now);
views->replies.textWidth = st::semiboldFont->width(
views->replies.text);
} else {
history()->owner().requestItemResize(this);
views->replies.text = (views->replies.count > 0)
? Lang::FormatCountToShort(views->replies.count).string
: QString();
views->replies.textWidth = views->replies.text.isEmpty()
? 0
: st::msgDateFont->width(views->replies.text);
}
if (forceResize || views->replies.textWidth != was) {
history()->owner().requestItemResize(this);
} else {
history()->owner().requestItemRepaint(this);
}
}
void HistoryMessage::changeRepliesCount(int delta, UserId replier) {
const auto views = Get<HistoryMessageViews>();
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
if (!views || views->replies.count < 0) {
return;
}
views->replies.count = std::max(views->replies.count + delta, 0);
if (replier && views->repliesChannelId) {
if (delta < 0) {
views->recentRepliers.erase(
ranges::remove(views->recentRepliers, replier),
end(views->recentRepliers));
} else if (!ranges::contains(views->recentRepliers, replier)) {
views->recentRepliers.insert(views->recentRepliers.begin(), replier);
while (views->recentRepliers.size() > limit) {
views->recentRepliers.pop_back();
}
}
}
refreshRepliesText(views);
}
void HistoryMessage::setReplyToTop(MsgId replyToTop) {
@ -1499,7 +1573,13 @@ void HistoryMessage::incrementReplyToTopCounter(
channelId,
reply->replyToTop());
if (top) {
top->setRepliesCount(top->repliesCount() + 1, 0);
if (const auto from = displayFrom()) {
if (const auto user = from->asUser()) {
top->changeRepliesCount(1, user->bareId());
return;
}
}
top->changeRepliesCount(1, UserId());
}
}
}
@ -1513,8 +1593,14 @@ void HistoryMessage::decrementReplyToTopCounter(
const auto top = history()->owner().message(
channelId,
reply->replyToTop());
if (const auto replies = (top ? top->repliesCount() : 0)) {
top->setRepliesCount(replies - 1, 0);
if (top) {
if (const auto from = displayFrom()) {
if (const auto user = from->asUser()) {
top->changeRepliesCount(-1, user->bareId());
return;
}
}
top->changeRepliesCount(-1, UserId());
}
}
}

View File

@ -19,6 +19,7 @@ class Message;
struct HistoryMessageEdited;
struct HistoryMessageReply;
struct HistoryMessageViews;
Fn<void(ChannelData*, MsgId)> HistoryDependentItemCallback(
not_null<HistoryItem*> item);
@ -133,7 +134,8 @@ public:
void setViewsCount(int count) override;
void setForwardsCount(int count) override;
void setRepliesCount(int count, int pts) override;
void setReplies(const MTPMessageReplies &data) override;
void changeRepliesCount(int delta, UserId replier) override;
void setReplyToTop(MsgId replyToTop) override;
void setRealId(MsgId newId) override;
void incrementReplyToTopCounter() override;
@ -165,6 +167,7 @@ public:
[[nodiscard]] int viewsCount() const override;
[[nodiscard]] int repliesCount() const override;
[[nodiscard]] bool repliesAreComments() const override;
bool updateDependencyItem() override;
[[nodiscard]] MsgId dependencyMsgId() const override {
return replyToId();
@ -206,6 +209,9 @@ private:
void setupForwardedComponent(const CreateConfig &config);
void incrementReplyToTopCounter(not_null<HistoryMessageReply*> reply);
void decrementReplyToTopCounter(not_null<HistoryMessageReply*> reply);
void refreshRepliesText(
not_null<HistoryMessageViews*> views,
bool forceResize = false);
static void FillForwardedInfo(
CreateConfig &config,

View File

@ -624,8 +624,12 @@ auto Element::verticalRepaintRange() const -> VerticalRepaintRange {
};
}
bool Element::hasHeavyPart() const {
return false;
}
void Element::checkHeavyPart() {
if (!_media || !_media->hasHeavyPart()) {
if (!hasHeavyPart() && (!_media || !_media->hasHeavyPart())) {
history()->owner().unregisterHeavyViewPart(this);
}
}

View File

@ -278,8 +278,9 @@ public:
};
[[nodiscard]] virtual VerticalRepaintRange verticalRepaintRange() const;
virtual bool hasHeavyPart() const;
virtual void unloadHeavyPart();
void checkHeavyPart();
void unloadHeavyPart();
// Legacy blocks structure.
HistoryBlock *block();

View File

@ -12,7 +12,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_message.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_web_page.h"
#include "history/view/history_view_replies_section.h"
#include "history/history.h"
#include "ui/effects/ripple_animation.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "ui/toast/toast.h"
@ -189,6 +191,19 @@ style::color FromNameFg(PeerId peerId, bool selected) {
} // namespace
struct Message::CommentsButton {
struct Userpic {
not_null<UserData*> user;
std::shared_ptr<Data::CloudImageView> view;
InMemoryKey uniqueKey;
};
std::unique_ptr<Ui::RippleAnimation> ripple;
std::vector<Userpic> userpics;
QImage cachedUserpics;
ClickHandlerPtr link;
QPoint lastPoint;
};
LogEntryOriginal::LogEntryOriginal() = default;
LogEntryOriginal::LogEntryOriginal(LogEntryOriginal &&other)
@ -211,6 +226,13 @@ Message::Message(
initPsa();
}
Message::~Message() {
if (_comments) {
_comments = nullptr;
checkHeavyPart();
}
}
not_null<HistoryMessage*> Message::message() const {
return static_cast<HistoryMessage*>(data().get());
}
@ -460,6 +482,9 @@ void Message::draw(
auto displayTail = skipTail ? RectPart::None : (outbg && !Core::App().settings().chatWide()) ? RectPart::Right : RectPart::Left;
PaintBubble(p, g, width(), selected, outbg, displayTail);
const auto gBubble = g;
paintCommentsButton(p, g, selected);
// Entry page is always a bubble bottom.
auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
@ -507,19 +532,28 @@ void Message::draw(
: true);
if (needDrawInfo) {
drawInfo(p, g.left() + g.width(), g.top() + g.height(), 2 * g.left() + g.width(), selected, InfoDisplayType::Default);
if (g != gBubble) {
const auto o = p.opacity();
p.setOpacity(0.3);
const auto color = selected
? (outbg ? st::msgOutDateFgSelected : st::msgInDateFgSelected)
: (outbg ? st::msgOutDateFg : st::msgInDateFg);
p.fillRect(g.left(), g.top() + g.height() - st::lineWidth, g.width(), st::lineWidth, color);
p.setOpacity(o);
}
}
if (displayRightAction()) {
const auto fastShareSkip = snap(
(g.height() - st::historyFastShareSize) / 2,
const auto fastShareSkip = std::clamp(
(gBubble.height() - st::historyFastShareSize) / 2,
0,
st::historyFastShareBottom);
const auto fastShareLeft = g.left() + g.width() + st::historyFastShareLeft;
const auto fastShareTop = g.top() + g.height() - fastShareSkip - st::historyFastShareSize;
const auto fastShareTop = g.top() + gBubble.height() - fastShareSkip - st::historyFastShareSize;
drawRightAction(p, fastShareLeft, fastShareTop, width());
}
if (media) {
media->paintBubbleFireworks(p, g, ms);
media->paintBubbleFireworks(p, gBubble, ms);
}
} else if (media && media->isDisplayed()) {
p.translate(g.topLeft());
@ -540,6 +574,137 @@ void Message::draw(
}
}
void Message::paintCommentsButton(
Painter &p,
QRect &g,
bool selected) const {
if (!data()->repliesAreComments()) {
return;
}
if (!_comments) {
_comments = std::make_unique<CommentsButton>();
history()->owner().registerHeavyViewPart(const_cast<Message*>(this));
}
const auto outbg = hasOutLayout();
const auto views = data()->Get<HistoryMessageViews>();
Assert(views != nullptr);
g.setHeight(g.height() - st::historyCommentsButtonHeight);
const auto top = g.top() + g.height();
auto left = g.left();
auto width = g.width();
if (_comments->ripple) {
p.setOpacity(st::historyPollRippleOpacity);
_comments->ripple->paint(p, left, top, width);
if (_comments->ripple->empty()) {
_comments->ripple.reset();
}
p.setOpacity(1.);
}
left += st::historyCommentsSkipLeft;
width -= st::historyCommentsSkipLeft
+ st::historyCommentsSkipRight;
const auto &open = outbg
? (selected ? st::historyCommentsOpenOutSelected : st::historyCommentsOpenOut)
: (selected ? st::historyCommentsOpenInSelected : st::historyCommentsOpenIn);
open.paint(p,
left + width - open.width(),
top + (st::historyCommentsButtonHeight - open.height()) / 2,
width);
if (views->recentRepliers.empty()) {
const auto &icon = outbg
? (selected ? st::historyCommentsOutSelected : st::historyCommentsOut)
: (selected ? st::historyCommentsInSelected : st::historyCommentsIn);
icon.paint(
p,
left,
top + (st::historyCommentsButtonHeight - icon.height()) / 2,
width);
left += icon.width();
} else {
auto &list = _comments->userpics;
const auto limit = HistoryMessageViews::kMaxRecentRepliers;
const auto count = std::min(int(views->recentRepliers.size()), limit);
const auto single = st::historyCommentsUserpicSize;
const auto shift = st::historyCommentsUserpicOverlap;
const auto regenerate = [&] {
if (list.size() != count) {
return true;
}
for (auto i = 0; i != count; ++i) {
auto &entry = list[i];
const auto user = entry.user;
auto &view = entry.view;
const auto wasView = view.get();
if (views->recentRepliers[i] != user->bareId()
|| user->userpicUniqueKey(view) != entry.uniqueKey
|| view.get() != wasView) {
return true;
}
}
return false;
}();
if (regenerate) {
for (auto i = 0; i != count; ++i) {
const auto userId = views->recentRepliers[i];
if (i == list.size()) {
list.push_back(CommentsButton::Userpic{
history()->owner().user(userId)
});
} else if (list[i].user->bareId() != userId) {
list[i].user = history()->owner().user(userId);
}
}
while (list.size() > count) {
list.pop_back();
}
const auto width = single + (limit - 1) * (single - shift);
if (_comments->cachedUserpics.isNull()) {
_comments->cachedUserpics = QImage(
QSize(width, single) * cIntRetinaFactor(),
QImage::Format_ARGB32_Premultiplied);
}
_comments->cachedUserpics.fill(Qt::transparent);
auto q = Painter(&_comments->cachedUserpics);
auto hq = PainterHighQualityEnabler(q);
auto pen = QPen(Qt::transparent);
pen.setWidth(st::historyCommentsUserpicStroke);
q.setBrush(Qt::NoBrush);
q.setPen(pen);
auto x = (count - 1) * (single - shift);
for (auto i = count; i != 0;) {
auto &entry = list[--i];
q.setCompositionMode(QPainter::CompositionMode_SourceOver);
entry.user->paintUserpic(q, entry.view, x, 0, single);
entry.uniqueKey = entry.user->userpicUniqueKey(entry.view);
q.setCompositionMode(QPainter::CompositionMode_Source);
q.drawEllipse(x, 0, single, single);
x -= single - shift;
}
}
p.drawImage(
left,
top + (st::historyCommentsButtonHeight - single) / 2,
_comments->cachedUserpics);
left += single + (count - 1) * (single - shift);
}
left += st::historyCommentsSkipText;
p.setPen(outbg ? (selected ? st::msgFileThumbLinkOutFgSelected : st::msgFileThumbLinkOutFg) : (selected ? st::msgFileThumbLinkInFgSelected : st::msgFileThumbLinkInFg));
p.setFont(st::semiboldFont);
p.drawTextLeft(
left,
top + (st::historyCommentsButtonHeight - st::semiboldFont->height) / 2,
width,
views->replies.text,
views->replies.textWidth);
}
void Message::paintFromName(
Painter &p,
QRect &trect,
@ -733,7 +898,7 @@ void Message::paintText(Painter &p, QRect &trect, TextSelection selection) const
}
PointState Message::pointState(QPoint point) const {
const auto g = countGeometry();
auto g = countGeometry();
if (g.width() < 1 || isHidden()) {
return PointState::Outside;
}
@ -752,6 +917,10 @@ PointState Message::pointState(QPoint point) const {
auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
if (item->repliesAreComments()) {
g.setHeight(g.height() - st::historyCommentsButtonHeight);
}
auto trect = g.marginsRemoved(st::msgPadding);
if (mediaOnBottom) {
trect.setHeight(trect.height() + st::msgPadding.bottom());
@ -788,6 +957,65 @@ bool Message::displayFromPhoto() const {
return hasFromPhoto() && !isAttachedToNext();
}
void Message::clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) {
Element::clickHandlerPressedChanged(handler, pressed);
if (!handler || !_comments) {
return;
} else if (handler == _comments->link) {
toggleCommentsButtonRipple(pressed);
}
}
void Message::toggleCommentsButtonRipple(bool pressed) {
Expects(_comments != nullptr);
if (!drawBubble()) {
return;
} else if (pressed) {
const auto g = countGeometry();
const auto linkWidth = g.width();
const auto linkHeight = st::historyCommentsButtonHeight;
if (!_comments->ripple) {
const auto drawMask = [&](QPainter &p) {
const auto radius = st::historyMessageRadius;
p.drawRoundedRect(
0,
0,
linkWidth,
linkHeight,
radius,
radius);
p.fillRect(0, 0, linkWidth, radius * 2, Qt::white);
};
auto mask = Ui::RippleAnimation::maskByDrawer(
QSize(linkWidth, linkHeight),
false,
drawMask);
_comments->ripple = std::make_unique<Ui::RippleAnimation>(
(hasOutLayout()
? st::historyPollRippleOut
: st::historyPollRippleIn),
std::move(mask),
[=] { history()->owner().requestViewRepaint(this); });
}
_comments->ripple->add(_comments->lastPoint);
} else if (_comments->ripple) {
_comments->ripple->lastStop();
}
}
bool Message::hasHeavyPart() const {
return _comments || Element::hasHeavyPart();
}
void Message::unloadHeavyPart() {
Element::unloadHeavyPart();
_comments = nullptr;
}
bool Message::hasFromPhoto() const {
if (isHidden()) {
return false;
@ -842,6 +1070,11 @@ TextState Message::textState(
auto mediaOnBottom = (mediaDisplayed && media->isBubbleBottom()) || (entry/* && entry->isBubbleBottom()*/);
auto mediaOnTop = (mediaDisplayed && media->isBubbleTop()) || (entry && entry->isBubbleTop());
const auto gBubble = g;
if (getStateCommentsButton(point, g, &result)) {
return result;
}
auto trect = g.marginsRemoved(st::msgPadding);
if (mediaOnBottom) {
trect.setHeight(trect.height() + st::msgPadding.bottom());
@ -913,11 +1146,11 @@ TextState Message::textState(
checkForPointInTime();
if (displayRightAction()) {
const auto fastShareSkip = snap(
(g.height() - st::historyFastShareSize) / 2,
(gBubble.height() - st::historyFastShareSize) / 2,
0,
st::historyFastShareBottom);
const auto fastShareLeft = g.left() + g.width() + st::historyFastShareLeft;
const auto fastShareTop = g.top() + g.height() - fastShareSkip - st::historyFastShareSize;
const auto fastShareTop = g.top() + gBubble.height() - fastShareSkip - st::historyFastShareSize;
if (QRect(
fastShareLeft,
fastShareTop,
@ -943,6 +1176,34 @@ TextState Message::textState(
return result;
}
bool Message::getStateCommentsButton(
QPoint point,
QRect &g,
not_null<TextState*> outResult) const {
if (!_comments) {
return false;
}
g.setHeight(g.height() - st::historyCommentsButtonHeight);
if (QRect(g.left(), g.top() + g.height(), g.width(), st::historyCommentsButtonHeight).contains(point)) {
if (!_comments->link) {
const auto fullId = data()->fullId();
_comments->link = std::make_shared<LambdaClickHandler>([=] {
if (const auto window = App::wnd()) {
if (const auto controller = window->sessionController()) {
if (const auto item = controller->session().data().message(fullId)) {
controller->showSection(
HistoryView::RepliesMemento(item->history(), item->id));
}
}
}
});
}
outResult->link = _comments->link;
_comments->lastPoint = point - QPoint(g.left(), g.top() + g.height());
}
return false;
}
bool Message::getStateFromName(
QPoint point,
QRect &trect,
@ -1344,32 +1605,67 @@ void Message::drawInfo(
}
if (auto views = item->Get<HistoryMessageViews>()) {
const auto showReplies = /*(views->views < 0) && */(views->replies > 0);
auto icon = [&] {
if (item->id > 0) {
if (outbg) {
auto left = infoRight - infoW;
const auto iconTop = infoBottom + st::historyViewsTop;
const auto textTop = infoBottom - st::msgDateFont->descent;
if (views->replies.count > 0 && !views->repliesChannelId) {
auto icon = [&] {
if (item->id > 0) {
if (outbg) {
return &(invertedsprites
? st::historyRepliesInvertedIcon
: selected
? st::historyRepliesOutSelectedIcon
: st::historyRepliesOutIcon);
}
return &(invertedsprites
? (showReplies ? st::historyRepliesInvertedIcon : st::historyViewsInvertedIcon)
? st::historyRepliesInvertedIcon
: selected
? (showReplies ? st::historyRepliesOutSelectedIcon : st::historyViewsOutSelectedIcon)
: (showReplies ? st::historyRepliesOutIcon : st::historyViewsOutIcon));
? st::historyRepliesInSelectedIcon
: st::historyRepliesInIcon);
}
return &(invertedsprites
? (showReplies ? st::historyRepliesInvertedIcon : st::historyViewsInvertedIcon)
: selected
? (showReplies ? st::historyRepliesInSelectedIcon : st::historyViewsInSelectedIcon)
: (showReplies ? st::historyRepliesInIcon : st::historyViewsInIcon));
? st::historyViewsSendingInvertedIcon
: st::historyViewsSendingIcon);
}();
if (item->id > 0) {
icon->paint(p, left, iconTop, width);
p.drawText(left + st::historyViewsWidth, textTop, views->replies.text);
} else if (!outbg && views->views.count < 0) { // sending outbg icon will be painted below
auto iconSkip = st::historyViewsSpace + views->replies.textWidth;
icon->paint(p, left + iconSkip, iconTop, width);
}
left += st::historyViewsSpace
+ views->replies.textWidth
+ st::historyViewsWidth;
}
if (views->views.count >= 0) {
auto icon = [&] {
if (item->id > 0) {
if (outbg) {
return &(invertedsprites
? st::historyViewsInvertedIcon
: selected
? st::historyViewsOutSelectedIcon
: st::historyViewsOutIcon);
}
return &(invertedsprites
? st::historyViewsInvertedIcon
: selected
? st::historyViewsInSelectedIcon
: st::historyViewsInIcon);
}
return &(invertedsprites
? st::historyViewsSendingInvertedIcon
: st::historyViewsSendingIcon);
}();
if (item->id > 0) {
icon->paint(p, left, iconTop, width);
p.drawText(left + st::historyViewsWidth, textTop, views->views.text);
} else if (!outbg) { // sending outbg icon will be painted below
auto iconSkip = st::historyViewsSpace + views->views.textWidth;
icon->paint(p, left + iconSkip, iconTop, width);
}
return &(invertedsprites
? st::historyViewsSendingInvertedIcon
: st::historyViewsSendingIcon);
}();
if (item->id > 0) {
icon->paint(p, infoRight - infoW, infoBottom + st::historyViewsTop, width);
p.drawText(infoRight - infoW + st::historyViewsWidth, infoBottom - st::msgDateFont->descent, views->text);
} else if (!outbg) { // sending outbg icon will be painted below
auto iconSkip = st::historyViewsSpace + views->textWidth;
icon->paint(p, infoRight - infoW + iconSkip, infoBottom + st::historyViewsTop, width);
}
} else if (item->id < 0 && item->history()->peer->isSelf() && !outbg) {
auto icon = &(invertedsprites ? st::historyViewsSendingInvertedIcon : st::historyViewsSendingIcon);
@ -1424,9 +1720,16 @@ int Message::infoWidth() const {
const auto item = message();
auto result = item->_timeWidth;
if (auto views = item->Get<HistoryMessageViews>()) {
result += st::historyViewsSpace
+ views->textWidth
+ st::historyViewsWidth;
if (views->views.count >= 0) {
result += st::historyViewsSpace
+ views->views.textWidth
+ st::historyViewsWidth;
}
if (views->replies.count > 0 && !views->repliesChannelId) {
result += st::historyViewsSpace
+ views->replies.textWidth
+ st::historyViewsWidth;
}
} else if (item->id < 0 && item->history()->peer->isSelf()) {
if (!hasOutLayout()) {
result += st::historySendStateSpace;
@ -1465,7 +1768,12 @@ int Message::timeLeft() const {
const auto item = message();
auto result = 0;
if (auto views = item->Get<HistoryMessageViews>()) {
result += st::historyViewsSpace + views->textWidth + st::historyViewsWidth;
if (views->views.count >= 0) {
result += st::historyViewsSpace + views->views.textWidth + st::historyViewsWidth;
}
if (views->replies.count > 0 && !views->repliesChannelId) {
result += st::historyViewsSpace + views->replies.textWidth + st::historyViewsWidth;
}
} else if (item->id < 0 && item->history()->peer->isSelf()) {
if (!hasOutLayout()) {
result += st::historySendStateSpace;
@ -1950,6 +2258,10 @@ int Message::resizeContentGetHeight(int newWidth) {
reply->resize(contentWidth - st::msgPadding.left() - st::msgPadding.right());
newHeight += st::msgReplyPadding.top() + st::msgReplyBarSize.height() + st::msgReplyPadding.bottom();
}
if (item->repliesAreComments()) {
newHeight += st::historyCommentsButtonHeight;
}
} else if (mediaDisplayed) {
newHeight = media->height();
} else {
@ -2007,18 +2319,6 @@ void Message::initTime() {
item->_timeText = dateTime().toString(cTimeFormat());
item->_timeWidth = st::msgDateFont->width(item->_timeText);
}
if (const auto views = item->Get<HistoryMessageViews>()) {
views->text = (views->views > 0)
? Lang::FormatCountToShort(views->views).string
: (views->views < 0)
? (views->replies > 0
? Lang::FormatCountToShort(views->replies).string
: QString())
: QString("1");
views->textWidth = views->text.isEmpty()
? 0
: st::msgDateFont->width(views->text);
}
if (item->_text.hasSkipBlock()) {
if (item->_text.updateSkipBlock(skipBlockWidth(), skipBlockHeight())) {
item->_textWidth = -1;

View File

@ -43,6 +43,11 @@ public:
not_null<ElementDelegate*> delegate,
not_null<HistoryMessage*> data,
Element *replacing);
~Message();
void clickHandlerPressedChanged(
const ClickHandlerPtr &handler,
bool pressed) override;
int marginTop() const override;
int marginBottom() const override;
@ -73,6 +78,9 @@ public:
TextSelection selection,
TextSelectType type) const override;
bool hasHeavyPart() const override;
void unloadHeavyPart() override;
// hasFromPhoto() returns true even if we don't display the photo
// but we need to skip a place at the left side for this photo
bool hasFromPhoto() const override;
@ -103,6 +111,8 @@ protected:
void refreshDataIdHook() override;
private:
struct CommentsButton;
not_null<HistoryMessage*> message() const;
void initLogEntryOriginal();
@ -115,6 +125,9 @@ private:
[[nodiscard]] TextSelection unskipTextSelection(
TextSelection selection) const;
void toggleCommentsButtonRipple(bool pressed);
void paintCommentsButton(Painter &p, QRect &g, bool selected) const;
void paintFromName(Painter &p, QRect &trect, bool selected) const;
void paintForwardedInfo(Painter &p, QRect &trect, bool selected) const;
void paintReplyInfo(Painter &p, QRect &trect, bool selected) const;
@ -122,6 +135,10 @@ private:
void paintViaBotIdInfo(Painter &p, QRect &trect, bool selected) const;
void paintText(Painter &p, QRect &trect, TextSelection selection) const;
bool getStateCommentsButton(
QPoint point,
QRect &g,
not_null<TextState*> outResult) const;
bool getStateFromName(
QPoint point,
QRect &trect,
@ -170,6 +187,7 @@ private:
mutable ClickHandlerPtr _rightActionLink;
mutable ClickHandlerPtr _fastReplyLink;
mutable std::unique_ptr<CommentsButton> _comments;
int _bubbleWidthLimit = 0;
};

View File

@ -105,6 +105,8 @@ RepliesWidget::RepliesWidget(
: Window::SectionWidget(parent, controller)
, _history(history)
, _rootId(rootId)
, _root(lookupRoot())
, _areComments(computeAreComments())
, _scroll(this, st::historyScroll, false)
, _topBar(this, controller)
, _topBarShadow(this)
@ -113,6 +115,8 @@ RepliesWidget::RepliesWidget(
controller,
ComposeControls::Mode::Normal))
, _scrollDown(_scroll, st::historyToDown) {
setupRoot();
_topBar->setActiveChat(_history, TopBarWidget::Section::Replies);
_topBar->move(0, 0);
@ -179,6 +183,33 @@ RepliesWidget::RepliesWidget(
RepliesWidget::~RepliesWidget() = default;
void RepliesWidget::setupRoot() {
if (_root) {
refreshRootView();
} else {
const auto channel = _history->peer->asChannel();
const auto done = crl::guard(this, [=](ChannelData*, MsgId) {
_root = lookupRoot();
if (_root) {
refreshRootView();
_areComments = computeAreComments();
}
});
_history->session().api().requestMessageData(channel, _rootId, done);
}
}
void RepliesWidget::refreshRootView() {
}
HistoryItem *RepliesWidget::lookupRoot() const {
return _history->owner().message(_history->channelId(), _rootId);
}
bool RepliesWidget::computeAreComments() const {
return _root && _root->isDiscussionPost();
}
void RepliesWidget::setupComposeControls() {
_composeControls->setHistory(_history);
@ -246,7 +277,7 @@ void RepliesWidget::setupComposeControls() {
) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
if (e->key() == Qt::Key_Up) {
if (!_composeControls->isEditingMessage()) {
// #TODO replies
// #TODO replies edit last sent message
//auto &messages = session().data().scheduledMessages();
//if (const auto item = messages.lastSentMessage(_history)) {
// _inner->editMessageRequestNotify(item->fullId());
@ -1064,13 +1095,19 @@ void RepliesWidget::restoreState(not_null<RepliesMemento*> memento) {
rpl::single(
tr::lng_contacts_loading()
) | rpl::then(_replies->fullCount(
) | rpl::map([=](int count) {
) | rpl::then(rpl::combine(
_replies->fullCount(),
_areComments.value()
) | rpl::map([=](int count, bool areComments) {
return count
? tr::lng_replies_header(
lt_count,
rpl::single(count) | tr::to_count())
: tr::lng_replies_header_none();
? (areComments
? tr::lng_comments_header
: tr::lng_replies_header)(
lt_count,
rpl::single(count) | tr::to_count())
: (areComments
? tr::lng_comments_header_none
: tr::lng_replies_header_none)();
})) | rpl::flatten_latest(
) | rpl::start_with_next([=](const QString &text) {
_topBar->setCustomTitle(text);

View File

@ -146,6 +146,8 @@ private:
void setupComposeControls();
void setupRoot();
void refreshRootView();
void setupDragArea();
void setupScrollDownButton();
@ -167,6 +169,8 @@ private:
void chooseAttach();
[[nodiscard]] SendMenu::Type sendMenuType() const;
[[nodiscard]] MsgId replyToId() const;
[[nodiscard]] HistoryItem *lookupRoot() const;
[[nodiscard]] bool computeAreComments() const;
void pushReplyReturn(not_null<HistoryItem*> item);
void computeCurrentReplyReturn();
@ -215,7 +219,9 @@ private:
const not_null<History*> _history;
const MsgId _rootId = 0;
HistoryItem *_root = nullptr;
std::shared_ptr<Data::RepliesList> _replies;
rpl::variable<bool> _areComments = false;
object_ptr<Ui::ScrollArea> _scroll;
QPointer<ListWidget> _inner;
object_ptr<TopBarWidget> _topBar;

View File

@ -362,7 +362,7 @@ void Gif::draw(Painter &p, const QRect &r, TextSelection selection, crl::time ms
auto roundRadius = isRound ? ImageRoundRadius::Ellipse : inWebPage ? ImageRoundRadius::Small : ImageRoundRadius::Large;
auto roundCorners = (isRound || inWebPage) ? RectPart::AllCorners : ((isBubbleTop() ? (RectPart::TopLeft | RectPart::TopRight) : RectPart::None)
| ((isBubbleBottom() && _caption.isEmpty()) ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None));
| ((isRoundedInBubbleBottom() && _caption.isEmpty()) ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None));
if (streamed) {
auto paused = autoPaused;
if (isRound) {
@ -1136,7 +1136,8 @@ bool Gif::needsBubble() const {
return true;
}
const auto item = _parent->data();
return item->viaBot()
return item->repliesAreComments()
|| item->viaBot()
|| _parent->displayedReply()
|| _parent->displayForwardedFrom()
|| _parent->displayFromName();

View File

@ -99,7 +99,7 @@ public:
QString additionalInfoString() const override;
bool skipBubbleTail() const override {
return isBubbleBottom() && _caption.isEmpty();
return isRoundedInBubbleBottom() && _caption.isEmpty();
}
bool isReadyForOpen() const override;

View File

@ -184,7 +184,7 @@ void Location::draw(Painter &p, const QRect &r, TextSelection selection, crl::ti
auto roundRadius = ImageRoundRadius::Large;
auto roundCorners = ((isBubbleTop() && _title.isEmpty() && _description.isEmpty()) ? (RectPart::TopLeft | RectPart::TopRight) : RectPart::None)
| (isBubbleBottom() ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None);
| (isRoundedInBubbleBottom() ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None);
auto rthumb = QRect(paintx, painty, paintw, painth);
ensureMediaCreated();
if (const auto thumbnail = _media->image()) {
@ -319,11 +319,11 @@ bool Location::needsBubble() const {
return true;
}
const auto item = _parent->data();
return item->viaBot()
return item->repliesAreComments()
|| item->viaBot()
|| _parent->displayedReply()
|| _parent->displayForwardedFrom()
|| _parent->displayFromName();
return false;
}
int Location::fullWidth() const {

View File

@ -55,7 +55,7 @@ public:
}
bool skipBubbleTail() const override {
return isBubbleBottom();
return isRoundedInBubbleBottom();
}
void unloadHeavyPart() override;

View File

@ -186,4 +186,8 @@ TextState Media::getStateGrouped(
Unexpected("Grouping method call.");
}
bool Media::isRoundedInBubbleBottom() const {
return isBubbleBottom() && !_parent->data()->repliesAreComments();
}
} // namespace HistoryView

View File

@ -217,6 +217,7 @@ public:
return (_inBubbleState == MediaInBubbleState::Bottom)
|| (_inBubbleState == MediaInBubbleState::None);
}
[[nodiscard]] bool isRoundedInBubbleBottom() const;
[[nodiscard]] virtual bool skipBubbleTail() const {
return false;
}

View File

@ -170,7 +170,7 @@ RectParts GroupedMedia::cornersFromSides(RectParts sides) const {
if (!isBubbleTop()) {
result &= ~(RectPart::TopLeft | RectPart::TopRight);
}
if (!isBubbleBottom() || !_caption.isEmpty()) {
if (!isRoundedInBubbleBottom() || !_caption.isEmpty()) {
result &= ~(RectPart::BottomLeft | RectPart::BottomRight);
}
return result;
@ -453,7 +453,8 @@ bool GroupedMedia::computeNeedBubble() const {
return true;
}
if (const auto item = _parent->data()) {
if (item->viaBot()
if (item->repliesAreComments()
|| item->viaBot()
|| _parent->displayedReply()
|| _parent->displayForwardedFrom()
|| _parent->displayFromName()

View File

@ -76,7 +76,7 @@ public:
HistoryMessageEdited *displayedEditBadge() const override;
bool skipBubbleTail() const override {
return isBubbleBottom() && _caption.isEmpty();
return isRoundedInBubbleBottom() && _caption.isEmpty();
}
void updateNeedBubbleState() override;
bool needsBubble() const override;

View File

@ -250,7 +250,7 @@ void Photo::draw(Painter &p, const QRect &r, TextSelection selection, crl::time
auto inWebPage = (_parent->media() != this);
auto roundRadius = inWebPage ? ImageRoundRadius::Small : ImageRoundRadius::Large;
auto roundCorners = inWebPage ? RectPart::AllCorners : ((isBubbleTop() ? (RectPart::TopLeft | RectPart::TopRight) : RectPart::None)
| ((isBubbleBottom() && _caption.isEmpty()) ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None));
| ((isRoundedInBubbleBottom() && _caption.isEmpty()) ? (RectPart::BottomLeft | RectPart::BottomRight) : RectPart::None));
const auto pix = [&] {
if (const auto large = _dataMedia->image(PhotoSize::Large)) {
return large->pixSingle(_pixw, _pixh, paintw, painth, roundRadius, roundCorners);
@ -801,7 +801,8 @@ bool Photo::needsBubble() const {
}
const auto item = _parent->data();
if (item->toHistoryMessage()) {
return item->viaBot()
return item->repliesAreComments()
|| item->viaBot()
|| _parent->displayedReply()
|| _parent->displayForwardedFrom()
|| _parent->displayFromName();

View File

@ -83,7 +83,7 @@ public:
return _caption.isEmpty();
}
bool skipBubbleTail() const override {
return isBubbleBottom() && _caption.isEmpty();
return isRoundedInBubbleBottom() && _caption.isEmpty();
}
bool isReadyForOpen() const override;

View File

@ -1518,7 +1518,7 @@ void Poll::toggleLinkRipple(bool pressed) {
radius);
p.fillRect(0, 0, linkWidth, radius * 2, Qt::white);
};
auto mask = isBubbleBottom()
auto mask = isRoundedInBubbleBottom()
? Ui::RippleAnimation::maskByDrawer(
QSize(linkWidth, linkHeight),
false,

View File

@ -1306,13 +1306,7 @@ void MainWidget::viewsIncrementDone(
item->setForwardsCount(forwards->v);
}
if (const auto replies = data.vreplies()) {
item->setRepliesCount(
replies->match([&](const MTPDmessageReplies &data) {
return data.vreplies().v;
}),
replies->match([&](const MTPDmessageReplies &data) {
return data.vreplies_pts().v;
}));
item->setReplies(*replies);
}
});
}