Marked ephemeral bot commands with hover hint in autocomplete.

This commit is contained in:
23rd
2026-06-11 13:28:36 +03:00
committed by John Preston
parent 967b0bc875
commit bd6dddfb5e
3 changed files with 174 additions and 5 deletions
@@ -860,6 +860,9 @@ mentionFg: windowSubTextFg;
mentionFgOver: windowSubTextFgOver;
mentionFgActive: windowActiveTextFg;
mentionFgOverActive: windowActiveTextFg;
mentionEphemeralIcon: icon {{ "chat/ephemeral_badge", mentionFg }};
mentionEphemeralIconOver: icon {{ "chat/ephemeral_badge", mentionFgOver }};
mentionEphemeralIconSkip: 5px;
autocompleteSearchPadding: margins(16px, 5px, 16px, 5px);
autocompleteRowPadding: margins(16px, 5px, 16px, 5px);
@@ -37,7 +37,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/scroll_area.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/tooltip.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/image/image.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/painter.h"
@@ -52,6 +55,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat.h"
#include "styles/style_widgets.h"
#include "styles/style_chat_helpers.h"
#include "styles/style_dialogs.h"
#include "styles/style_menu_icons.h"
#include <QtWidgets/QApplication>
@@ -59,6 +63,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace ChatHelpers {
namespace {
constexpr auto kEphemeralHintHoverDelay = crl::time(500);
[[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) {
const auto &usernames = user->usernames();
return usernames.empty() ? user->username() : usernames.front();
@@ -111,6 +117,7 @@ public:
botCommandChosen() const;
rpl::producer<FieldAutocomplete::StickerChosen> stickerChosen() const;
rpl::producer<ScrollTo> scrollToRequested() const;
rpl::producer<QRect> ephemeralIconHovered() const;
void onParentGeometryChanged();
@@ -132,6 +139,8 @@ private:
void setSel(int sel, bool scroll = false);
void showPreview();
void selectByMouse(QPoint global);
[[nodiscard]] QRect ephemeralIconRect(int index) const;
void updateEphemeralIconHover(QPoint position);
QSize stickerBoundingBox() const;
void setupLottie(StickerSuggestion &suggestion);
@@ -158,6 +167,7 @@ private:
int _stickersPerRow = 1;
int _sel = -1;
int _down = -1;
int _ephemeralIconHover = -1;
std::optional<QPoint> _lastMousePosition;
bool _mouseSelection = false;
@@ -177,6 +187,7 @@ private:
rpl::event_stream<FieldAutocomplete::BotCommandChosen> _botCommandChosen;
rpl::event_stream<FieldAutocomplete::StickerChosen> _stickerChosen;
rpl::event_stream<ScrollTo> _scrollToRequested;
rpl::event_stream<QRect> _ephemeralIconHovered;
base::Timer _previewTimer;
@@ -214,6 +225,7 @@ struct FieldAutocomplete::BotCommandRow {
QString description;
Ui::PeerUserpicView userpic;
Ui::Text::String descriptionText;
bool ephemeral = false;
};
FieldAutocomplete::FieldAutocomplete(
@@ -224,7 +236,8 @@ FieldAutocomplete::FieldAutocomplete(
, _show(std::move(show))
, _session(&_show->session())
, _st(stOverride ? *stOverride : st::defaultEmojiPan)
, _scroll(this) {
, _scroll(this)
, _ephemeralHintTimer([=] { showPendingEphemeralHint(); }) {
hide();
_scroll->setGeometry(rect());
@@ -245,6 +258,16 @@ FieldAutocomplete::FieldAutocomplete(
_scroll->scrollToY(data.top, data.bottom);
}, lifetime());
_scroll->scrollTopValue(
) | rpl::skip(1) | rpl::on_next([=] {
hideEphemeralHint();
}, lifetime());
_inner->ephemeralIconHovered(
) | rpl::on_next([=](QRect iconRect) {
ephemeralIconHovered(iconRect);
}, lifetime());
_scroll->show();
_inner->show();
@@ -671,10 +694,11 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) {
not_null<UserData*> user,
const Data::BotCommand &command) {
return BotCommandRow{
user,
command.command,
command.description,
user->activeUserpicView()
.user = user,
.command = command.command,
.description = command.description,
.userpic = user->activeUserpicView(),
.ephemeral = command.ephemeral,
};
};
brows.reserve(cnt);
@@ -787,6 +811,58 @@ void FieldAutocomplete::rowsUpdated(
_inner->rowsUpdated();
}
void FieldAutocomplete::createEphemeralHint(QRect rect) {
const auto parent = parentWidget();
_ephemeralHint = base::make_unique_q<Ui::ImportantTooltip>(
parent,
object_ptr<Ui::PaddingWrap<Ui::FlatLabel>>(
parent,
Ui::MakeNiceTooltipLabel(
parent,
tr::lng_ephemeral_command_tooltip(Ui::Text::WithEntities),
st::dialogsStoriesTooltipMaxWidth,
st::ttlMediaImportantTooltipLabel),
st::defaultImportantTooltip.padding),
st::dialogsStoriesTooltip);
_ephemeralHint->pointAt(rect, RectPart::Top);
_ephemeralHint->toggleAnimated(true);
}
void FieldAutocomplete::ephemeralIconHovered(QRect iconRect) {
const auto parent = parentWidget();
if (iconRect.isEmpty() || !parent || isHidden() || _hiding) {
hideEphemeralHint();
return;
}
_ephemeralHintRect = Ui::MapFrom(parent, _inner.data(), iconRect);
if (_ephemeralHint && !_ephemeralHint->isHidden()) {
_ephemeralHint->pointAt(_ephemeralHintRect, RectPart::Top);
_ephemeralHint->toggleAnimated(true);
} else {
_ephemeralHintTimer.callOnce(kEphemeralHintHoverDelay);
}
}
void FieldAutocomplete::showPendingEphemeralHint() {
if (_ephemeralHintRect.isEmpty() || isHidden() || _hiding) {
return;
}
if (_ephemeralHint) {
_ephemeralHint->pointAt(_ephemeralHintRect, RectPart::Top);
_ephemeralHint->toggleAnimated(true);
} else {
createEphemeralHint(_ephemeralHintRect);
}
}
void FieldAutocomplete::hideEphemeralHint() {
_ephemeralHintRect = QRect();
_ephemeralHintTimer.cancel();
if (_ephemeralHint) {
_ephemeralHint->toggleAnimated(false);
}
}
void FieldAutocomplete::setBoundings(QRect boundings) {
_boundings = boundings;
recount();
@@ -829,6 +905,8 @@ void FieldAutocomplete::recount(bool resetScroll) {
}
void FieldAutocomplete::hideFast() {
hideEphemeralHint();
_ephemeralHint = nullptr;
_a_opacity.stop();
hideFinish();
}
@@ -837,6 +915,7 @@ void FieldAutocomplete::hideAnimated() {
if (isHidden() || _hiding) {
return;
}
hideEphemeralHint();
if (_cache.isNull()) {
_scroll->show();
@@ -1209,6 +1288,19 @@ void FieldAutocomplete::Inner::paintEvent(QPaintEvent *e) {
auto addleft = commandTextWidth + st::mentionPadding.left();
auto widthleft = mentionwidth - addleft;
if (row.ephemeral) {
const auto &icon = selected
? st::mentionEphemeralIconOver
: st::mentionEphemeralIcon;
icon.paint(
p,
mentionleft + addleft,
(i * st::mentionHeight
+ (st::mentionHeight - icon.height()) / 2),
width());
addleft += icon.width() + st::mentionEphemeralIconSkip;
widthleft -= icon.width() + st::mentionEphemeralIconSkip;
}
if (!row.description.isEmpty()
&& row.descriptionText.isEmpty()) {
row.descriptionText.setText(
@@ -1243,6 +1335,7 @@ void FieldAutocomplete::Inner::resizeEvent(QResizeEvent *e) {
void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {
const auto globalPosition = e->globalPos();
updateEphemeralIconHover(e->pos());
if (!_lastMousePosition) {
_lastMousePosition = globalPosition;
return;
@@ -1253,6 +1346,57 @@ void FieldAutocomplete::Inner::mouseMoveEvent(QMouseEvent *e) {
selectByMouse(globalPosition);
}
void FieldAutocomplete::Inner::updateEphemeralIconHover(QPoint position) {
const auto inCommands = !_brows->empty()
&& _srows->empty()
&& _mrows->empty()
&& _hrows->empty();
const auto index = inCommands ? (position.y() / st::mentionHeight) : -1;
const auto good = (index >= 0)
&& (index < int(_brows->size()))
&& _brows->at(index).ephemeral
&& ephemeralIconRect(index).contains(position);
const auto hovered = good ? index : -1;
if (_ephemeralIconHover == hovered) {
return;
}
_ephemeralIconHover = hovered;
_ephemeralIconHovered.fire(good
? ephemeralIconRect(hovered)
: QRect());
}
QRect FieldAutocomplete::Inner::ephemeralIconRect(int index) const {
const auto &row = _brows->at(index);
if (!row.ephemeral) {
return QRect();
}
const auto filter = _parent->filter();
const auto hasUsername = filter.indexOf('@') > 0;
const auto botStatus = _parent->chat()
? _parent->chat()->botStatus
: ((_parent->channel() && _parent->channel()->isMegagroup())
? _parent->channel()->mgInfo->botStatus
: Data::BotStatus::NoBots);
auto toHighlight = row.command;
if (hasUsername || botStatus != Data::BotStatus::NoBots) {
toHighlight += '@' + PrimaryUsername(row.user);
}
const auto mentionleft = 2 * st::mentionPadding.left()
+ st::mentionPhotoSize;
const auto left = mentionleft
+ st::semiboldFont->width('/' + toHighlight)
+ st::mentionPadding.left();
const auto top = index * st::mentionHeight
+ (st::mentionHeight - st::mentionEphemeralIcon.height()) / 2;
return QRect(
left,
top,
st::mentionEphemeralIcon.width(),
st::mentionEphemeralIcon.height());
}
void FieldAutocomplete::Inner::clearSel(bool hidden) {
_overDelete = false;
_mouseSelection = false;
@@ -1478,6 +1622,10 @@ void FieldAutocomplete::Inner::enterEventHook(QEnterEvent *e) {
void FieldAutocomplete::Inner::leaveEventHook(QEvent *e) {
setMouseTracking(false);
if (_ephemeralIconHover >= 0) {
_ephemeralIconHover = -1;
_ephemeralIconHovered.fire(QRect());
}
if (_mouseSelection) {
setSel(-1);
_mouseSelection = false;
@@ -1536,6 +1684,10 @@ void FieldAutocomplete::Inner::rowsUpdated() {
if (_srows->empty()) {
_stickersLifetime.destroy();
}
if (_ephemeralIconHover >= 0) {
_ephemeralIconHover = -1;
_ephemeralIconHovered.fire(QRect());
}
}
auto FieldAutocomplete::Inner::getLottieRenderer()
@@ -1721,6 +1873,10 @@ auto FieldAutocomplete::Inner::scrollToRequested() const
return _scrollToRequested.events();
}
rpl::producer<QRect> FieldAutocomplete::Inner::ephemeralIconHovered() const {
return _ephemeralIconHovered.events();
}
void InitFieldAutocomplete(
std::unique_ptr<FieldAutocomplete> &autocomplete,
FieldAutocompleteDescriptor &&descriptor) {
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "base/timer.h"
#include "base/object_ptr.h"
#include "base/unique_qptr.h"
namespace style {
struct EmojiPan;
@@ -22,6 +23,7 @@ namespace Ui {
class PopupMenu;
class ScrollArea;
class InputField;
class ImportantTooltip;
} // namespace Ui
namespace Lottie {
@@ -165,6 +167,10 @@ private:
void updateFiltered(bool resetScroll = false);
void recount(bool resetScroll = false);
StickerRows getStickerSuggestions();
void createEphemeralHint(QRect rect);
void ephemeralIconHovered(QRect iconRect);
void showPendingEphemeralHint();
void hideEphemeralHint();
const std::shared_ptr<Show> _show;
const not_null<Main::Session*> _session;
@@ -197,6 +203,10 @@ private:
bool _hiding = false;
base::unique_qptr<Ui::ImportantTooltip> _ephemeralHint;
base::Timer _ephemeralHintTimer;
QRect _ephemeralHintRect;
Ui::Animations::Simple _a_opacity;
rpl::event_stream<> _refreshRequests;
rpl::event_stream<> _stickersUpdateRequests;