diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 87d6c5b54f..ef0c27be8d 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -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); diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp index c3654bc80b..a2afb17672 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp @@ -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 @@ -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 user) { const auto &usernames = user->usernames(); return usernames.empty() ? user->username() : usernames.front(); @@ -111,6 +117,7 @@ public: botCommandChosen() const; rpl::producer stickerChosen() const; rpl::producer scrollToRequested() const; + rpl::producer 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 _lastMousePosition; bool _mouseSelection = false; @@ -177,6 +187,7 @@ private: rpl::event_stream _botCommandChosen; rpl::event_stream _stickerChosen; rpl::event_stream _scrollToRequested; + rpl::event_stream _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 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( + parent, + object_ptr>( + 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 FieldAutocomplete::Inner::ephemeralIconHovered() const { + return _ephemeralIconHovered.events(); +} + void InitFieldAutocomplete( std::unique_ptr &autocomplete, FieldAutocompleteDescriptor &&descriptor) { diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h index 89184bc033..88099164cc 100644 --- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.h +++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.h @@ -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; const not_null _session; @@ -197,6 +203,10 @@ private: bool _hiding = false; + base::unique_qptr _ephemeralHint; + base::Timer _ephemeralHintTimer; + QRect _ephemeralHintRect; + Ui::Animations::Simple _a_opacity; rpl::event_stream<> _refreshRequests; rpl::event_stream<> _stickersUpdateRequests;