Support nice Reply button in the corner.

This commit is contained in:
John Preston
2026-02-20 14:28:57 +04:00
parent 0f6a5480df
commit a83890b03e
16 changed files with 739 additions and 125 deletions
+2
View File
@@ -957,6 +957,8 @@ PRIVATE
history/view/history_view_reaction_preview.h
history/view/history_view_reply.cpp
history/view/history_view_reply.h
history/view/history_view_reply_button.cpp
history/view/history_view_reply_button.h
history/view/history_view_requests_bar.cpp
history/view/history_view_requests_bar.h
history/view/history_view_schedule_box.cpp
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_common.h"
#include "data/data_peer_values.h"
#include "data/data_channel.h"
#include "data/data_changes.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "base/unixtime.h"
@@ -872,10 +873,17 @@ void EditRestrictedBox::prepare() {
restrictWeak->closeBox();
}
};
const auto savedUser = user();
const auto savedPeer = peer();
const auto done = [=](
ChatAdminRightsInfo,
const QString &) {
ChatAdminRightsInfo newRights,
const QString &rank) {
closeBoth();
savedUser->session().changes().chatAdminChanged(
savedPeer,
savedUser,
newRights.flags,
rank);
};
const auto fail = closeBoth;
adminBox->setSaveCallback(
@@ -2013,6 +2013,11 @@ void ParticipantsBoxController::editAdminDone(
if (_editParticipantBox) {
_editParticipantBox->closeBox();
}
_additional.applyAdminLocally(user, rights, rank);
recomputeTypeFor(user);
refreshRows();
const auto flags = rights.flags;
user->session().changes().chatAdminChanged(_peer, user, flags, rank);
}
@@ -22,12 +22,12 @@ ResolveWindow ResolveWindowDefault() {
return [](not_null<Main::Session*> session)
-> Window::SessionController* {
const auto check = [&](Window::Controller *window) {
if (const auto controller = window->sessionController()) {
if (&controller->session() == session) {
return controller;
}
}
return (Window::SessionController*)nullptr;
const auto controller = window
? window->sessionController()
: nullptr;
return (controller && (&controller->session() == session))
? controller
: nullptr;
};
auto &app = Core::App();
const auto account = not_null(&session->account());
@@ -25,6 +25,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_message.h"
#include "history/view/history_view_service_message.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_reply_button.h"
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_reaction_preview.h"
#include "history/view/history_view_quick_action.h"
@@ -331,6 +332,9 @@ HistoryInner::HistoryInner(
std::make_unique<HistoryView::Reactions::Manager>(
this,
[=](QRect updated) { update(updated); }))
, _replyButtonManager(
std::make_unique<HistoryView::ReplyButton::Manager>(
[=](QRect updated) { update(updated); }))
, _touchSelectTimer([=] { onTouchSelect(); })
, _touchScrollTimer([=] { onTouchScrollTimer(); })
, _middleClickAutoscroll(
@@ -1475,6 +1479,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
return true;
});
_replyButtonManager->paint(p, context);
_reactionsManager->paint(p, context);
}
@@ -2086,6 +2091,7 @@ void HistoryInner::performDrag() {
if (auto mimeData = prepareDrag()) {
// This call enters event loop and can destroy any QObject.
_reactionsManager->updateButton({});
_replyButtonManager->updateButton({});
_controller->widget()->launchDrag(
std::move(mimeData),
crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));
@@ -2104,6 +2110,7 @@ void HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {
}
_animatedStickersPlayed.remove(item);
_reactionsManager->remove(item->fullId());
_replyButtonManager->remove(item->fullId());
auto i = _selected.find(item);
if (i != _selected.cend()) {
@@ -3937,6 +3944,7 @@ void HistoryInner::enterEventHook(QEnterEvent *e) {
void HistoryInner::leaveEventHook(QEvent *e) {
_reactionsManager->updateButton({ .cursorLeft = true });
_replyButtonManager->updateButton({});
if (auto item = Element::Hovered()) {
repaintItem(item);
Element::Hovered(nullptr);
@@ -4293,7 +4301,28 @@ auto HistoryInner::reactionButtonParameters(
auto result = view->reactionButtonParameters(
position,
reactionState
).translated({ 0, itemTop(view) });
).translated({ 0, top });
result.visibleTop = _visibleAreaTop;
result.visibleBottom = _visibleAreaBottom;
result.globalPointer = _mousePosition;
return result;
}
auto HistoryInner::replyButtonParameters(
not_null<const Element*> view,
QPoint position,
const HistoryView::TextState &replyState) const
-> HistoryView::ReplyButton::ButtonParameters {
const auto top = itemTop(view);
if (top < 0
|| _mouseAction == MouseAction::Dragging
|| inSelectionMode().inSelectionMode) {
return {};
}
auto result = view->replyButtonParameters(
position,
replyState
).translated({ 0, top });
result.visibleTop = _visibleAreaTop;
result.visibleBottom = _visibleAreaBottom;
result.globalPointer = _mousePosition;
@@ -4315,8 +4344,15 @@ void HistoryInner::mouseActionUpdate() {
const auto reactionState = _reactionsManager->buttonTextState(point);
const auto reactionItem = session().data().message(reactionState.itemId);
const auto reactionView = viewByItem(reactionItem);
const auto replyBtnState = reactionView
? HistoryView::TextState()
: _replyButtonManager->buttonTextState(point);
const auto replyBtnItem = session().data().message(replyBtnState.itemId);
const auto replyBtnView = viewByItem(replyBtnItem);
const auto view = reactionView
? reactionView
: replyBtnView
? replyBtnView
: (_aboutView
&& _aboutView->view()
&& point.y() >= _aboutView->top
@@ -4342,6 +4378,10 @@ void HistoryInner::mouseActionUpdate() {
view,
m,
reactionState));
_replyButtonManager->updateButton(replyButtonParameters(
view,
m,
replyBtnState));
if (changed) {
_reactionsItem = item;
}
@@ -4361,6 +4401,7 @@ void HistoryInner::mouseActionUpdate() {
Element::Moused(nullptr);
}
_reactionsManager->updateButton({});
_replyButtonManager->updateButton({});
}
if (_mouseActionItem && !viewByItem(_mouseActionItem)) {
mouseActionCancel();
@@ -4374,9 +4415,13 @@ void HistoryInner::mouseActionUpdate() {
&& !_selected.empty()
&& (_selected.cbegin()->second != FullSelection);
const auto overReaction = reactionView && reactionState.link;
const auto overReplyBtn = replyBtnView && replyBtnState.link;
if (overReaction) {
dragState = reactionState;
lnkhost = reactionView;
} else if (overReplyBtn) {
dragState = replyBtnState;
lnkhost = replyBtnView;
} else if (item) {
if (item != _mouseActionItem || ((m + selectionViewOffset) - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
if (_mouseAction == MouseAction::PrepareDrag) {
@@ -51,6 +51,11 @@ struct ChosenReaction;
struct ButtonParameters;
} // namespace HistoryView::Reactions
namespace HistoryView::ReplyButton {
class Manager;
struct ButtonParameters;
} // namespace HistoryView::ReplyButton
namespace Window {
class SessionController;
} // namespace Window
@@ -439,6 +444,11 @@ private:
QPoint position,
const HistoryView::TextState &reactionState) const
-> HistoryView::Reactions::ButtonParameters;
[[nodiscard]] auto replyButtonParameters(
not_null<const Element*> view,
QPoint position,
const HistoryView::TextState &replyState) const
-> HistoryView::ReplyButton::ButtonParameters;
void toggleFavoriteReaction(not_null<Element*> view) const;
void reactionChosen(const ChosenReaction &reaction);
@@ -512,6 +522,7 @@ private:
std::unique_ptr<HistoryView::Reactions::Manager> _reactionsManager;
rpl::variable<HistoryItem*> _reactionsItem;
std::unique_ptr<HistoryView::ReplyButton::Manager> _replyButtonManager;
HistoryItem *_pinnedItem = nullptr;
MouseAction _mouseAction = MouseAction::None;
@@ -20,6 +20,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_no_forwards_request.h"
#include "history/view/media/history_view_suggest_decision.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/history_view_reply_button.h"
#include "history/view/reactions/history_view_reactions.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_reply.h"
@@ -2800,6 +2801,12 @@ Reactions::ButtonParameters Element::reactionButtonParameters(
return {};
}
ReplyButton::ButtonParameters Element::replyButtonParameters(
QPoint position,
const TextState &replyState) const {
return {};
}
int Element::reactionsOptimalWidth() const {
return 0;
}
@@ -46,6 +46,10 @@ struct ButtonParameters;
class InlineList;
} // namespace HistoryView::Reactions
namespace HistoryView::ReplyButton {
struct ButtonParameters;
} // namespace HistoryView::ReplyButton
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
@@ -542,6 +546,9 @@ public:
[[nodiscard]] virtual auto reactionButtonParameters(
QPoint position,
const TextState &reactionState) const -> Reactions::ButtonParameters;
[[nodiscard]] virtual auto replyButtonParameters(
QPoint position,
const TextState &replyState) const -> ReplyButton::ButtonParameters;
[[nodiscard]] virtual int reactionsOptimalWidth() const;
// ClickHandlerHost interface.
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_sticker.h"
#include "history/view/reactions/history_view_reactions.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/history_view_reply_button.h"
#include "history/view/reactions/history_view_reactions_selector.h"
#include "history/view/history_view_context_menu.h"
#include "history/view/history_view_element.h"
@@ -405,6 +406,8 @@ ListWidget::ListWidget(
, _reactionsManager(_delegate->listMakeReactionsManager(
this,
[=](QRect updated) { update(updated); }))
, _replyButtonManager(std::make_unique<ReplyButton::Manager>(
[=](QRect updated) { update(updated); }))
, _translateTracker(MaybeTranslateTracker(_delegate->listTranslateHistory()))
, _scrollDateCheck([this] { scrollDateCheck(); })
, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
@@ -2384,6 +2387,9 @@ void ListWidget::paintEvent(QPaintEvent *e) {
paintUserpics(p, context, clip);
paintDates(p, context, clip);
if (_replyButtonManager) {
_replyButtonManager->paint(p, context);
}
if (_reactionsManager) {
_reactionsManager->paint(p, context);
}
@@ -3298,6 +3304,9 @@ void ListWidget::leaveEventHook(QEvent *e) {
if (_reactionsManager) {
_reactionsManager->updateButton({ .cursorLeft = true });
}
if (_replyButtonManager) {
_replyButtonManager->updateButton({});
}
if (const auto view = _overElement) {
if (_overState.pointState != PointState::Outside) {
repaintItem(view);
@@ -3604,7 +3613,27 @@ Reactions::ButtonParameters ListWidget::reactionButtonParameters(
auto result = view->reactionButtonParameters(
position,
reactionState
).translated({ 0, itemTop(view) });
).translated({ 0, top });
result.visibleTop = _visibleTop;
result.visibleBottom = _visibleBottom;
result.globalPointer = _mousePosition;
return result;
}
ReplyButton::ButtonParameters ListWidget::replyButtonParameters(
not_null<const Element*> view,
QPoint position,
const TextState &replyState) const {
const auto top = itemTop(view);
if (top < 0
|| _mouseAction == MouseAction::Dragging
|| inSelectionMode().inSelectionMode) {
return {};
}
auto result = view->replyButtonParameters(
position,
replyState
).translated({ 0, top });
result.visibleTop = _visibleTop;
result.visibleBottom = _visibleBottom;
result.globalPointer = _mousePosition;
@@ -3757,8 +3786,15 @@ void ListWidget::mouseActionUpdate() {
: TextState();
const auto reactionItem = session().data().message(reactionState.itemId);
const auto reactionView = viewForItem(reactionItem);
const auto replyBtnState = (_replyButtonManager && !reactionView)
? _replyButtonManager->buttonTextState(point)
: TextState();
const auto replyBtnItem = session().data().message(replyBtnState.itemId);
const auto replyBtnView = viewForItem(replyBtnItem);
const auto view = reactionView
? reactionView
: replyBtnView
? replyBtnView
: strictFindItemByY(point.y());
const auto item = view ? view->data().get() : nullptr;
if (view) {
@@ -3785,6 +3821,11 @@ void ListWidget::mouseActionUpdate() {
reactionState)
: Reactions::ButtonParameters());
}
if (_replyButtonManager) {
_replyButtonManager->updateButton(view
? replyButtonParameters(view, itemPoint, replyBtnState)
: ReplyButton::ButtonParameters());
}
if (viewChanged && view) {
_reactionsItem = item;
}
@@ -3795,10 +3836,14 @@ void ListWidget::mouseActionUpdate() {
&& (_overState.itemId == _pressState.itemId)
&& hasSelectedText();
auto dragStateUserpic = false;
const auto overReplyBtn = replyBtnView && replyBtnState.link;
const auto overReaction = reactionView && reactionState.link;
if (overReaction) {
dragState = reactionState;
lnkhost = reactionView;
} else if (overReplyBtn) {
dragState = replyBtnState;
lnkhost = replyBtnView;
} else if (view) {
auto cursorDeltaLength = [&] {
auto cursorDelta = (_overState.point - _pressState.point);
@@ -4301,6 +4346,9 @@ void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
if (_reactionsManager) {
_reactionsManager->remove(item->fullId());
}
if (_replyButtonManager) {
_replyButtonManager->remove(item->fullId());
}
updateItemsGeometry();
}
@@ -53,6 +53,11 @@ struct ChosenReaction;
struct ButtonParameters;
} // namespace HistoryView::Reactions
namespace HistoryView::ReplyButton {
class Manager;
struct ButtonParameters;
} // namespace HistoryView::ReplyButton
namespace Window {
struct SectionShow;
} // namespace Window
@@ -388,6 +393,10 @@ public:
not_null<const Element*> view,
QPoint position,
const TextState &reactionState) const;
[[nodiscard]] ReplyButton::ButtonParameters replyButtonParameters(
not_null<const Element*> view,
QPoint position,
const TextState &replyState) const;
void toggleFavoriteReaction(not_null<Element*> view) const;
@@ -781,6 +790,8 @@ private:
rpl::variable<HistoryItem*> _reactionsItem;
bool _useCornerReaction = false;
std::unique_ptr<ReplyButton::Manager> _replyButtonManager;
std::unique_ptr<TranslateTracker> _translateTracker;
int _minHeight = 0;
@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_suggest_decision.h"
#include "history/view/reactions/history_view_reactions.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/history_view_reply_button.h"
#include "history/view/history_view_group_call_bar.h" // UserpicInRow.
#include "history/view/history_view_reply.h"
#include "history/view/history_view_transcribe_button.h"
@@ -63,19 +64,6 @@ constexpr auto kSummarizeThreshold = 512;
constexpr auto kPlayStatusLimit = 2;
const auto kPsaTooltipPrefix = "cloud_lng_tooltip_psa_";
QString FastForwardText() {
return u"Forward"_q;
}
QString FastReplyText() {
return tr::lng_fast_reply(tr::now);
}
bool ShowFastForwardFor(const QString &username) {
return !username.compare(u"ReviewInsightsBot"_q, Qt::CaseInsensitive)
|| !username.compare(u"reviews_bot"_q, Qt::CaseInsensitive);
}
[[nodiscard]] ClickHandlerPtr MakeTopicButtonLink(
not_null<Data::ForumTopic*> topic,
MsgId messageId) {
@@ -754,16 +742,8 @@ QSize Message::performCountOptimalSize() {
namew += st::msgServiceFont->spacew + via->maxWidth
+ (_fromNameStatus ? st::msgServiceFont->spacew : 0);
}
const auto replyWidth = hasFastForward()
? st::msgFont->width(FastForwardText())
: hasFastReply()
? st::msgFont->width(FastReplyText())
: 0;
if (Has<RightBadge>()) {
namew += st::msgPadding.right()
+ std::max(rightBadgeWidth(), replyWidth);
} else if (replyWidth) {
namew += st::msgPadding.right() + replyWidth;
namew += st::msgPadding.right() + rightBadgeWidth();
}
accumulate_max(maxWidth, namew);
} else if (via && !displayForwardedFrom()) {
@@ -1649,21 +1629,10 @@ void Message::paintFromName(
return;
}
const auto badgeWidth = rightBadgeWidth();
const auto replyWidth = [&] {
if (isUnderCursor()) {
if (displayFastForward()) {
return st::msgFont->width(FastForwardText());
} else if (displayFastReply()) {
return st::msgFont->width(FastReplyText());
}
}
return 0;
}();
const auto rightWidth = replyWidth ? replyWidth : badgeWidth;
auto availableLeft = trect.left();
auto availableWidth = trect.width();
if (rightWidth) {
availableWidth -= st::msgPadding.right() + rightWidth;
if (badgeWidth) {
availableWidth -= st::msgPadding.right() + badgeWidth;
}
const auto stm = context.messageStyle();
@@ -1746,17 +1715,9 @@ void Message::paintFromName(
availableLeft += skipWidth;
availableWidth -= skipWidth;
}
if (rightWidth) {
if (badgeWidth) {
p.setPen(stm->msgDateFg);
if (replyWidth) {
p.setFont(ClickHandler::showAsActive(_fastReplyLink)
? st::msgFont->underline()
: st::msgFont);
p.drawText(
trect.left() + trect.width() - rightWidth,
trect.top() + st::msgFont->ascent,
hasFastForward() ? FastForwardText() : FastReplyText());
} else if (const auto badge = Get<RightBadge>()) {
if (const auto badge = Get<RightBadge>()) {
const auto badgeColor = (badge->role == BadgeRole::Creator)
? st::rankOwnerFg->c
: (badge->role == BadgeRole::Admin)
@@ -2779,29 +2740,12 @@ bool Message::getStateFromName(
if (!displayFromName()) {
return false;
}
const auto replyWidth = [&] {
if (isUnderCursor()) {
if (displayFastForward()) {
return st::msgFont->width(FastForwardText());
} else if (displayFastReply()) {
return st::msgFont->width(FastReplyText());
}
}
return 0;
}();
if (replyWidth
&& point.x() >= trect.left() + trect.width() - replyWidth
&& point.x() < trect.left() + trect.width() + st::msgPadding.right()
&& point.y() >= trect.top() - st::msgPadding.top()
&& point.y() < trect.top() + st::msgServiceFont->height) {
outResult->link = fastReplyLink();
return true;
}
if (point.y() >= trect.top() && point.y() < trect.top() + st::msgNameFont->height) {
auto availableLeft = trect.left();
auto availableWidth = trect.width();
if (replyWidth) {
availableWidth -= st::msgPadding.right() + replyWidth;
const auto badgeWidth = rightBadgeWidth();
if (badgeWidth) {
availableWidth -= st::msgPadding.right() + badgeWidth;
}
const auto item = data();
const auto from = item->displayFrom();
@@ -2849,6 +2793,50 @@ bool Message::getStateFromName(
outResult->link = via->link;
return true;
}
if (badgeWidth) {
const auto badge = Get<RightBadge>();
const auto badgeLeft = trect.left()
+ trect.width()
- badgeWidth;
const auto badgeRight = trect.left()
+ trect.width()
+ st::msgPadding.right();
const auto boostTextWidth = (badge && !badge->boosts.isEmpty())
? badge->boosts.maxWidth()
: 0;
const auto boostLeft = boostTextWidth
? (trect.left() + trect.width() - boostTextWidth)
: 0;
if (boostTextWidth
&& point.x() >= boostLeft
&& point.x() < badgeRight) {
if (!badge->boostsLink) {
badge->boostsLink = std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
if (const auto controller = ExtractController(context)) {
controller->showToast(u"Boosts clicked"_q);
}
});
}
outResult->link = badge->boostsLink;
return true;
}
const auto tagRight = boostTextWidth
? (boostLeft - st::msgTagBadgeBoostSkip)
: badgeRight;
if (point.x() >= badgeLeft && point.x() < tagRight) {
if (!badge->tagLink) {
badge->tagLink = std::make_shared<LambdaClickHandler>([=](
ClickContext context) {
if (const auto controller = ExtractController(context)) {
controller->showToast(u"Tag clicked"_q);
}
});
}
outResult->link = badge->tagLink;
return true;
}
}
}
trect.setTop(trect.top() + st::msgNameFont->height);
return false;
@@ -3400,6 +3388,31 @@ Reactions::ButtonParameters Message::reactionButtonParameters(
return result;
}
ReplyButton::ButtonParameters Message::replyButtonParameters(
QPoint position,
const TextState &replyState) const {
using namespace ReplyButton;
if (!displayFastReply() || unwrapped()) {
return {};
}
auto result = ButtonParameters{ .context = data()->fullId() };
const auto geometry = countGeometry();
result.pointer = position;
const auto reactionInnerRight = st::reactionCornerCenter.x()
+ st::reactionCornerSize.width() / 2;
const auto replyInnerWidth = ReplyButton::ComputeInnerWidth();
const auto relativeCenter = QPoint(
geometry.width() + reactionInnerRight - replyInnerWidth,
st::replyCornerCenter.y());
result.center = geometry.topLeft() + relativeCenter;
if (replyState.itemId != result.context
&& !geometry.contains(position)) {
result.outside = true;
}
result.link = fastReplyLink();
return result;
}
int Message::reactionsOptimalWidth() const {
return _reactions ? _reactions->countNiceWidth() : 0;
}
@@ -3856,22 +3869,6 @@ bool Message::hasFastReply() const {
return !hasOutLayout() && (peer->isChat() || peer->isMegagroup());
}
bool Message::hasFastForward() const {
if (context() != Context::History) {
return false;
}
const auto item = data();
const auto from = item->from()->asUser();
if (!from || !from->isBot() || !ShowFastForwardFor(from->username())) {
return false;
}
const auto peer = item->history()->peer;
if (!peer->isChat() && !peer->isMegagroup()) {
return false;
}
return !hasOutLayout();
}
bool Message::displayFastReply() const {
const auto canSendAnything = [&] {
const auto item = data();
@@ -3888,12 +3885,6 @@ bool Message::displayFastReply() const {
&& !delegate()->elementInSelectionMode(this).inSelectionMode;
}
bool Message::displayFastForward() const {
return hasFastForward()
&& data()->allowsForward()
&& !delegate()->elementInSelectionMode(this).inSelectionMode;
}
bool Message::displayRightActionComments() const {
return !isPinnedContext()
&& (context() != Context::SavedSublist)
@@ -4178,20 +4169,7 @@ ClickHandlerPtr Message::fastReplyLink() const {
return _fastReplyLink;
}
const auto itemId = data()->fullId();
const auto sessionId = data()->history()->session().uniqueId();
_fastReplyLink = hasFastForward()
? std::make_shared<LambdaClickHandler>([=](ClickContext context) {
const auto controller = ExtractController(context);
const auto session = controller
? &controller->session()
: nullptr;
if (!session || session->uniqueId() != sessionId) {
return;
} else if (const auto item = session->data().message(itemId)) {
FastShareMessage(controller, item);
}
})
: std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
_fastReplyLink = std::make_shared<LambdaClickHandler>(crl::guard(this, [=] {
delegate()->elementReplyTo({ itemId });
}));
return _fastReplyLink;
@@ -4282,16 +4260,8 @@ void Message::updateMediaInBubbleState() {
void Message::fromNameUpdated(int width) const {
const auto item = data();
const auto replyWidth = hasFastForward()
? st::msgFont->width(FastForwardText())
: hasFastReply()
? st::msgFont->width(FastReplyText())
: 0;
if (Has<RightBadge>()) {
width -= st::msgPadding.right()
+ std::max(rightBadgeWidth(), replyWidth);
} else if (replyWidth) {
width -= st::msgPadding.right() + replyWidth;
width -= st::msgPadding.right() + rightBadgeWidth();
}
const auto from = item->displayFrom();
validateFromNameText(from);
@@ -37,6 +37,10 @@ namespace Reactions {
class InlineList;
} // namespace Reactions
namespace ReplyButton {
struct ButtonParameters;
} // namespace ReplyButton
// Special type of Component for the channel actions log.
struct LogEntryOriginal : RuntimeComponent<LogEntryOriginal, Element> {
LogEntryOriginal();
@@ -68,6 +72,8 @@ enum class BadgeRole : uchar {
struct RightBadge : RuntimeComponent<RightBadge, Element> {
Ui::Text::String tag;
Ui::Text::String boosts;
mutable ClickHandlerPtr tagLink;
mutable ClickHandlerPtr boostsLink;
int width = 0;
BadgeRole role = BadgeRole::User;
bool overridden = false;
@@ -126,6 +132,9 @@ public:
Reactions::ButtonParameters reactionButtonParameters(
QPoint position,
const TextState &reactionState) const override;
ReplyButton::ButtonParameters replyButtonParameters(
QPoint position,
const TextState &replyState) const override;
int reactionsOptimalWidth() const override;
void unloadHeavyPart() override;
@@ -300,9 +309,7 @@ private:
[[nodiscard]] bool needInfoDisplay() const;
[[nodiscard]] bool invertMedia() const;
[[nodiscard]] bool hasFastReply() const;
[[nodiscard]] bool hasFastForward() const;
[[nodiscard]] bool displayFastReply() const;
[[nodiscard]] bool displayFastForward() const;
[[nodiscard]] bool isPinnedContext() const;
[[nodiscard]] bool isCommentsRootView() const;
@@ -0,0 +1,350 @@
/*
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_reply_button.h"
#include "history/view/history_view_cursor_state.h"
#include "ui/chat/chat_style.h"
#include "lang/lang_keys.h"
#include "styles/style_chat.h"
namespace HistoryView::ReplyButton {
namespace {
constexpr auto kToggleDuration = crl::time(120);
constexpr auto kButtonShowDelay = crl::time(300);
constexpr auto kButtonHideDelay = crl::time(300);
[[nodiscard]] float64 ScaleForState(ButtonState state) {
switch (state) {
case ButtonState::Hidden: return 0.;
case ButtonState::Shown:
case ButtonState::Active:
case ButtonState::Inside: return 1.;
}
Unexpected("State in ReplyButton::ScaleForState.");
}
[[nodiscard]] float64 OpacityForScale(float64 scale) {
return scale;
}
[[nodiscard]] QSize ComputeInnerSize() {
return QSize(ComputeInnerWidth(), st::replyCornerHeight);
}
[[nodiscard]] QSize ComputeOuterSize() {
return QRect(
QPoint(),
ComputeInnerSize()
).marginsAdded(st::replyCornerShadow).size();
}
} // namespace
int ComputeInnerWidth() {
struct Cached {
QString text;
int result = 0;
};
static auto cached = Cached();
const auto &text = tr::lng_fast_reply(tr::now);
if (cached.text != text) {
const auto &padding = st::replyCornerTextPadding;
const auto textWidth = st::msgDateTextStyle.font->width(text);
cached.result = padding.left() + textWidth + padding.right();
cached.text = text;
}
return cached.result;
}
Button::Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void()> hide,
QSize outer)
: _update(std::move(update))
, _finalScale(ScaleForState(_state))
, _collapsed(QPoint(), outer)
, _geometry(_collapsed)
, _hideTimer(hide) {
applyParameters(parameters, nullptr);
}
Button::~Button() = default;
bool Button::isHidden() const {
return (_state == ButtonState::Hidden)
&& !_opacityAnimation.animating();
}
QRect Button::geometry() const {
return _geometry;
}
float64 Button::currentScale() const {
return _scaleAnimation.value(_finalScale);
}
float64 Button::currentOpacity() const {
return _opacityAnimation.value(
OpacityForScale(ScaleForState(_state)));
}
void Button::applyParameters(ButtonParameters parameters) {
applyParameters(std::move(parameters), _update);
}
void Button::applyParameters(
ButtonParameters parameters,
Fn<void(QRect)> update) {
const auto shift = parameters.center - _collapsed.center();
_collapsed = _collapsed.translated(shift);
updateGeometry(update);
applyState(ButtonState::Shown, update);
if (parameters.outside) {
_hideTimer.callOnce(kButtonHideDelay);
} else {
_hideTimer.cancel();
}
}
void Button::updateGeometry(Fn<void(QRect)> update) {
if (_geometry != _collapsed) {
if (update) {
update(_geometry);
}
_geometry = _collapsed;
if (update) {
update(_geometry);
}
}
}
void Button::applyState(ButtonState state) {
applyState(state, _update);
}
void Button::applyState(ButtonState state, Fn<void(QRect)> update) {
if (state == ButtonState::Hidden) {
_hideTimer.cancel();
}
updateGeometry(update);
if (_state == state) {
return;
}
const auto finalScale = ScaleForState(state);
_opacityAnimation.start(
[=] { _update(_geometry); },
OpacityForScale(ScaleForState(_state)),
OpacityForScale(ScaleForState(state)),
kToggleDuration,
anim::sineInOut);
if (state != ButtonState::Hidden && _finalScale != finalScale) {
_scaleAnimation.start(
[=] { _update(_geometry); },
_finalScale,
finalScale,
kToggleDuration,
anim::sineInOut);
_finalScale = finalScale;
}
_state = state;
}
Manager::Manager(Fn<void(QRect)> buttonUpdate)
: _outer(ComputeOuterSize())
, _inner(QRect(QPoint(), ComputeInnerSize()))
, _cachedRound(
ComputeInnerSize(),
st::replyCornerShadow,
ComputeInnerSize().height())
, _buttonShowTimer([=] { showButtonDelayed(); })
, _buttonUpdate(std::move(buttonUpdate))
, _text(st::msgDateTextStyle.font->width(tr::lng_fast_reply(tr::now))) {
_text.setText(st::msgDateTextStyle, tr::lng_fast_reply(tr::now));
_inner.translate(
QRect(QPoint(), _outer).center() - _inner.center());
}
Manager::~Manager() = default;
void Manager::updateButton(ButtonParameters parameters) {
const auto contextChanged = (_buttonContext != parameters.context);
if (contextChanged) {
if (_button) {
_button->applyState(ButtonState::Hidden);
_buttonHiding.push_back(std::move(_button));
}
_buttonShowTimer.cancel();
_scheduledParameters = std::nullopt;
}
_buttonContext = parameters.context;
if (parameters.link) {
_link = parameters.link;
}
if (!_buttonContext) {
return;
} else if (_button) {
_button->applyParameters(parameters);
return;
} else if (parameters.outside) {
_buttonShowTimer.cancel();
_scheduledParameters = std::nullopt;
return;
}
const auto globalPositionChanged = _scheduledParameters
&& (_scheduledParameters->globalPointer
!= parameters.globalPointer);
const auto positionChanged = _scheduledParameters
&& (_scheduledParameters->pointer != parameters.pointer);
_scheduledParameters = parameters;
if ((_buttonShowTimer.isActive() && positionChanged)
|| globalPositionChanged) {
_buttonShowTimer.callOnce(kButtonShowDelay);
}
}
void Manager::showButtonDelayed() {
clearAppearAnimations();
_button = std::make_unique<Button>(
_buttonUpdate,
*_scheduledParameters,
[=] { updateButton({}); },
_outer);
}
void Manager::paint(QPainter &p, const PaintContext &context) {
removeStaleButtons();
for (const auto &button : _buttonHiding) {
paintButton(p, context, button.get());
}
if (const auto current = _button.get()) {
if (context.gestureHorizontal.ratio) {
current->applyState(ButtonState::Hidden);
_buttonHiding.push_back(std::move(_button));
}
paintButton(p, context, current);
}
}
void Manager::paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button) {
const auto geometry = button->geometry();
if (!context.clip.intersects(geometry)) {
return;
}
constexpr auto kFramesCount = Ui::RoundAreaWithShadow::kFramesCount;
const auto scale = button->currentScale();
const auto scaleMin = ScaleForState(ButtonState::Hidden);
const auto scaleMax = ScaleForState(ButtonState::Shown);
const auto progress = (scale - scaleMin) / (scaleMax - scaleMin);
const auto frame = int(
base::SafeRound(progress * (kFramesCount - 1)));
const auto useScale = scaleMin
+ (frame / float64(kFramesCount - 1))
* (scaleMax - scaleMin);
paintButton(p, context, button, frame, useScale);
}
void Manager::paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button,
int frameIndex,
float64 scale) {
const auto opacity = button->currentOpacity();
if (opacity == 0.) {
return;
}
const auto geometry = button->geometry();
const auto position = geometry.topLeft();
if (opacity != 1.) {
p.setOpacity(opacity);
}
const auto shadow = context.st->shadowFg()->c;
const auto background = context.st->windowBg()->c;
_cachedRound.setShadowColor(shadow);
_cachedRound.setBackgroundColor(background);
const auto radius = _inner.height() / 2.;
const auto frame = _cachedRound.validateFrame(
frameIndex,
scale,
radius);
p.drawImage(position, *frame.image, frame.rect);
const auto textLeft = position.x()
+ _inner.x()
+ st::replyCornerTextPadding.left();
const auto textTop = position.y()
+ _inner.y()
+ (_inner.height() - st::msgDateTextStyle.font->height) / 2;
const auto &incomingStyle = context.st->messageStyle(false, false);
p.setPen(incomingStyle.msgDateFg);
p.setFont(st::msgDateTextStyle.font);
_text.draw(p, {
.position = QPoint(textLeft, textTop),
.availableWidth = _text.maxWidth(),
});
if (opacity != 1.) {
p.setOpacity(1.);
}
}
TextState Manager::buttonTextState(QPoint position) const {
if (overCurrentButton(position)) {
auto result = TextState(nullptr, _link);
result.itemId = _buttonContext;
return result;
}
return {};
}
bool Manager::overCurrentButton(QPoint position) const {
if (!_button) {
return false;
}
return buttonInner().contains(position);
}
QMargins Manager::innerMargins() const {
return {
_inner.x(),
_inner.y(),
_outer.width() - _inner.x() - _inner.width(),
_outer.height() - _inner.y() - _inner.height(),
};
}
QRect Manager::buttonInner() const {
return buttonInner(_button.get());
}
QRect Manager::buttonInner(not_null<Button*> button) const {
return button->geometry().marginsRemoved(innerMargins());
}
void Manager::remove(FullMsgId context) {
if (_buttonContext == context) {
_buttonContext = {};
_button = nullptr;
}
}
void Manager::removeStaleButtons() {
_buttonHiding.erase(
ranges::remove_if(_buttonHiding, &Button::isHidden),
end(_buttonHiding));
}
void Manager::clearAppearAnimations() {
_buttonHiding.clear();
}
} // namespace HistoryView::ReplyButton
@@ -0,0 +1,138 @@
/*
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
*/
#pragma once
#include "base/timer.h"
#include "base/weak_ptr.h"
#include "ui/effects/animations.h"
#include "ui/effects/round_area_with_shadow.h"
#include "ui/text/text.h"
namespace Ui {
struct ChatPaintContext;
} // namespace Ui
namespace HistoryView {
using PaintContext = Ui::ChatPaintContext;
struct TextState;
} // namespace HistoryView
namespace HistoryView::ReplyButton {
struct ButtonParameters {
[[nodiscard]] ButtonParameters translated(QPoint delta) const {
auto result = *this;
result.center += delta;
result.pointer += delta;
return result;
}
FullMsgId context;
QPoint center;
QPoint pointer;
QPoint globalPointer;
ClickHandlerPtr link;
int visibleTop = 0;
int visibleBottom = 0;
bool outside = false;
};
[[nodiscard]] int ComputeInnerWidth();
enum class ButtonState {
Hidden,
Shown,
Active,
Inside,
};
class Button final {
public:
Button(
Fn<void(QRect)> update,
ButtonParameters parameters,
Fn<void()> hide,
QSize outer);
~Button();
void applyParameters(ButtonParameters parameters);
void applyState(ButtonState state);
[[nodiscard]] bool isHidden() const;
[[nodiscard]] QRect geometry() const;
[[nodiscard]] float64 currentScale() const;
[[nodiscard]] float64 currentOpacity() const;
private:
void updateGeometry(Fn<void(QRect)> update);
void applyState(ButtonState state, Fn<void(QRect)> update);
void applyParameters(
ButtonParameters parameters,
Fn<void(QRect)> update);
const Fn<void(QRect)> _update;
ButtonState _state = ButtonState::Hidden;
float64 _finalScale = 0.;
Ui::Animations::Simple _scaleAnimation;
Ui::Animations::Simple _opacityAnimation;
QRect _collapsed;
QRect _geometry;
base::Timer _hideTimer;
};
class Manager final : public base::has_weak_ptr {
public:
Manager(Fn<void(QRect)> buttonUpdate);
~Manager();
void updateButton(ButtonParameters parameters);
void paint(QPainter &p, const PaintContext &context);
[[nodiscard]] TextState buttonTextState(QPoint position) const;
void remove(FullMsgId context);
private:
void showButtonDelayed();
[[nodiscard]] bool overCurrentButton(QPoint position) const;
void paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button);
void paintButton(
QPainter &p,
const PaintContext &context,
not_null<Button*> button,
int frame,
float64 scale);
void removeStaleButtons();
void clearAppearAnimations();
[[nodiscard]] QMargins innerMargins() const;
[[nodiscard]] QRect buttonInner() const;
[[nodiscard]] QRect buttonInner(not_null<Button*> button) const;
QSize _outer;
QRect _inner;
Ui::RoundAreaWithShadow _cachedRound;
ClickHandlerPtr _link;
FullMsgId _buttonContext;
std::optional<ButtonParameters> _scheduledParameters;
base::Timer _buttonShowTimer;
const Fn<void(QRect)> _buttonUpdate;
std::unique_ptr<Button> _button;
std::vector<std::unique_ptr<Button>> _buttonHiding;
Ui::Text::String _text;
};
} // namespace HistoryView::ReplyButton
@@ -218,7 +218,7 @@ UnwrappedMedia::SurroundingInfo UnwrappedMedia::surroundingInfo(
panelHeight += reply->height()
- ((forwarded || via) ? 0 : replyMargins.top())
- replyMargins.bottom();
} else {
} else if (panelHeight) {
panelHeight += st::msgReplyPadding.bottom();
}
const auto total = (topicSize.isEmpty() ? 0 : topicSize.height())
+5
View File
@@ -916,6 +916,11 @@ reactionMainAppearShift: 20px;
reactionCollapseFadeThreshold: 40px;
reactionFlyUp: 50px;
replyCornerHeight: 20px;
replyCornerShadow: margins(4px, 8px, 4px, 8px);
replyCornerCenter: point(7px, -2px);
replyCornerTextPadding: margins(6px, 0px, 6px, 0px);
effectInfoImage: 12px;
searchInChatMultiSelectItem: MultiSelectItem(defaultMultiSelectItem) {