diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 2288910cb..54ecda43a 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1475,6 +1475,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_profile_enable_notifications" = "Notifications"; "lng_profile_send_message" = "Send Message"; "lng_profile_open_app" = "Open App"; +"lng_profile_open_app_short" = "Open"; "lng_profile_open_app_about" = "By launching this mini app, you agree to the {terms}."; "lng_profile_open_app_terms" = "Terms of Service for Mini Apps"; "lng_profile_bot_permissions_title" = "Allow access to"; diff --git a/Telegram/SourceFiles/boxes/stickers_box.cpp b/Telegram/SourceFiles/boxes/stickers_box.cpp index 7509b898c..778db60af 100644 --- a/Telegram/SourceFiles/boxes/stickers_box.cpp +++ b/Telegram/SourceFiles/boxes/stickers_box.cpp @@ -1826,8 +1826,8 @@ void StickersBox::Inner::setPressed(SelectedRow pressed) { if (_megagroupSet && pressedIndex >= 0 && pressedIndex < _rows.size()) { update(0, _itemsTop + pressedIndex * _rowHeight, width(), _rowHeight); auto &set = _rows[pressedIndex]; - auto rippleMask = Ui::RippleAnimation::RectMask(QSize(width(), _rowHeight)); if (!set->ripple) { + auto rippleMask = Ui::RippleAnimation::RectMask(QSize(width(), _rowHeight)); set->ripple = std::make_unique(st::defaultRippleAnimation, std::move(rippleMask), [this, pressedIndex] { update(0, _itemsTop + pressedIndex * _rowHeight, width(), _rowHeight); }); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index a0b8c4c76..2cc193d59 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -108,6 +108,10 @@ taggedForumDialogRow: DialogRow(forumDialogRow) { } dialogRowFilterTagSkip : 4px; dialogRowFilterTagFont : font(10px); +dialogRowOpenBotTextStyle: semiboldTextStyle; +dialogRowOpenBotHeight: 20px; +dialogRowOpenBotRight: 10px; +dialogRowOpenBotTop: 32px; forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }}; forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }}; diff --git a/Telegram/SourceFiles/dialogs/dialogs_entry.h b/Telegram/SourceFiles/dialogs/dialogs_entry.h index e1909f7a6..b97044241 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_entry.h +++ b/Telegram/SourceFiles/dialogs/dialogs_entry.h @@ -29,6 +29,7 @@ class SavedSublist; } // namespace Data namespace Ui { +class RippleAnimation; struct PeerUserpicView; } // namespace Ui @@ -43,6 +44,14 @@ class Row; class IndexedList; class MainList; +struct RightButton final { + QImage bg; + QImage selectedBg; + QImage activeBg; + Ui::Text::String text; + std::unique_ptr ripple; +}; + struct RowsByLetter { not_null main; base::flat_map> letters; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 9bcbb39d8..363b0dfd6 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -29,6 +29,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/text_options.h" #include "ui/dynamic_thumbnails.h" #include "ui/painter.h" +#include "ui/rect.h" #include "ui/ui_utility.h" #include "data/data_drafts.h" #include "data/data_folder.h" @@ -62,6 +63,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "window/window_controller.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" +#include "ui/effects/ripple_animation.h" #include "ui/effects/loading_element.h" #include "ui/widgets/multi_select.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" @@ -122,6 +124,19 @@ constexpr auto kPreviewPostsLimit = 3; return result; } +[[nodiscard]] UserData *MaybeBotWithApp(Row *row) { + if (row) { + if (const auto history = row->key().history()) { + if (const auto user = history->peer->asUser()) { + if (user->botInfo && user->botInfo->hasMainApp) { + return user; + } + } + } + } + return nullptr; +} + [[nodiscard]] object_ptr MakeSearchEmpty( QWidget *parent, SearchState state) { @@ -773,7 +788,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { not_null row, bool selected, bool mayBeActive) { - const auto key = row->key(); + const auto &key = row->key(); const auto active = mayBeActive && isRowActive(row, activeEntry); const auto forum = key.history() && key.history()->isForum(); if (forum && !_topicJumpCache) { @@ -781,6 +796,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } const auto expanding = forum && (key.history()->peer->id == childListShown.peerId); + context.rightButton = maybeCacheRightButton(row); context.st = (forum ? &st::forumDialogRow : _st.get()); @@ -1195,6 +1211,47 @@ void InnerWidget::paintEvent(QPaintEvent *e) { } } +[[nodiscard]] RightButton *InnerWidget::maybeCacheRightButton(Row *row) { + if (const auto user = MaybeBotWithApp(row)) { + const auto it = _rightButtons.find(user->id); + if (it == _rightButtons.end()) { + auto rightButton = RightButton(); + const auto text + = tr::lng_profile_open_app_short(tr::now).toUpper(); + rightButton.text.setText(st::dialogRowOpenBotTextStyle, text); + const auto size = QSize( + rightButton.text.maxWidth() + + rightButton.text.minHeight(), + st::dialogRowOpenBotHeight); + const auto generateBg = [&](const style::color &c) { + auto bg = QImage( + style::DevicePixelRatio() * size, + QImage::Format_ARGB32_Premultiplied); + bg.setDevicePixelRatio(style::DevicePixelRatio()); + bg.fill(Qt::transparent); + { + auto p = QPainter(&bg); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(c); + const auto r = size.height() / 2; + p.drawRoundedRect(Rect(size), r, r); + } + return bg; + }; + rightButton.bg = generateBg(st::activeButtonBg); + rightButton.selectedBg = generateBg(st::activeButtonBgOver); + rightButton.activeBg = generateBg(st::activeButtonFg); + return &(_rightButtons.emplace( + user->id, + std::move(rightButton)).first->second); + } else { + return &(it->second); + } + } + return nullptr; +} + Ui::VideoUserpic *InnerWidget::validateVideoUserpic(not_null row) { const auto history = row->history(); return history ? validateVideoUserpic(history) : nullptr; @@ -1554,6 +1611,26 @@ void InnerWidget::clearIrrelevantState() { } } +bool InnerWidget::lookupIsInBotAppButton( + Row *row, + QPoint localPosition) { + if (const auto user = MaybeBotWithApp(row)) { + const auto it = _rightButtons.find(user->id); + if (it != _rightButtons.end()) { + const auto s = it->second.bg.size() / style::DevicePixelRatio(); + const auto r = QRect( + width() - s.width() - st::dialogRowOpenBotRight, + st::dialogRowOpenBotTop, + s.width(), + s.height()); + if (r.contains(localPosition)) { + return true; + } + } + } + return false; +} + void InnerWidget::selectByMouse(QPoint globalPosition) { const auto local = mapFromGlobal(globalPosition); if (updateReorderPinned(local)) { @@ -1594,16 +1671,19 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { : (mouseY >= offset) ? _shownList->rowAtY(mouseY - offset) : nullptr; + const auto mappedY = selected ? mouseY - offset - selected->top() : 0; const auto selectedTopicJump = selected - && selected->lookupIsInTopicJump( - local.x(), - mouseY - offset - selected->top()); + && selected->lookupIsInTopicJump(local.x(), mappedY); + const auto selectedBotApp = selected + && lookupIsInBotAppButton(selected, QPoint(local.x(), mappedY)); if (_collapsedSelected != collapsedSelected || _selected != selected - || _selectedTopicJump != selectedTopicJump) { + || _selectedTopicJump != selectedTopicJump + || _selectedBotApp != selectedBotApp) { updateSelectedRow(); _selected = selected; _selectedTopicJump = selectedTopicJump; + _selectedBotApp = selectedBotApp; _collapsedSelected = collapsedSelected; updateSelectedRow(); setCursor((_selected || _collapsedSelected >= 0) @@ -1729,7 +1809,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { selectByMouse(e->globalPos()); _pressButton = e->button(); - setPressed(_selected, _selectedTopicJump); + setPressed(_selected, _selectedTopicJump, _selectedBotApp); setCollapsedPressed(_collapsedSelected); setHashtagPressed(_hashtagSelected); _hashtagDeletePressed = _hashtagDeleteSelected; @@ -1762,7 +1842,22 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { }; const auto origin = e->pos() - QPoint(0, dialogsOffset() + _pressed->top()); - if (_pressedTopicJump) { + if (_pressedBotApp && _pressedBotAppData) { + const auto size = _pressedBotAppData->bg.size() + / style::DevicePixelRatio(); + if (!_pressedBotAppData->ripple) { + const auto r = size.height() / 2; + _pressedBotAppData->ripple + = std::make_unique( + st::defaultRippleAnimation, + Ui::RippleAnimation::RoundRectMask(size, r), + updateCallback); + } + const auto shift = QPoint( + width() - size.width() - st::dialogRowOpenBotRight, + st::dialogRowOpenBotTop); + _pressedBotAppData->ripple->add(origin - shift); + } else if (_pressedTopicJump) { row->addTopicJumpRipple( origin, _topicJumpCache.get(), @@ -2094,6 +2189,7 @@ void InnerWidget::mousePressReleased( setCollapsedPressed(-1); const auto pressedTopicRootId = _pressedTopicJumpRootId; const auto pressedTopicJump = _pressedTopicJump; + const auto pressedBotApp = _pressedBotApp; auto pressed = _pressed; clearPressed(); auto hashtagPressed = _hashtagPressed; @@ -2113,12 +2209,16 @@ void InnerWidget::mousePressReleased( if (wasDragging) { selectByMouse(globalPosition); } + if (_pressedBotAppData && _pressedBotAppData->ripple) { + _pressedBotAppData->ripple->lastStop(); + } updateSelectedRow(); if (!wasDragging && button == Qt::LeftButton) { if ((collapsedPressed >= 0 && collapsedPressed == _collapsedSelected) || (pressed && pressed == _selected - && pressedTopicJump == _selectedTopicJump) + && pressedTopicJump == _selectedTopicJump + && pressedBotApp == _selectedBotApp) || (hashtagPressed >= 0 && hashtagPressed == _hashtagSelected && hashtagDeletePressed == _hashtagDeleteSelected) @@ -2131,7 +2231,13 @@ void InnerWidget::mousePressReleased( && searchedPressed == _searchedSelected) || (pressedMorePosts && pressedMorePosts == _selectedMorePosts)) { - chooseRow(modifiers, pressedTopicRootId); + if (pressedBotApp) { + if (const auto user = MaybeBotWithApp(pressed)) { + _openBotMainAppRequests.fire(peerToUser(user->id)); + } + } else { + chooseRow(modifiers, pressedTopicRootId); + } } } if (auto activated = ClickHandler::unpressed()) { @@ -2152,14 +2258,31 @@ void InnerWidget::setCollapsedPressed(int pressed) { } } -void InnerWidget::setPressed(Row *pressed, bool pressedTopicJump) { - if (_pressed != pressed || (pressed && _pressedTopicJump != pressedTopicJump)) { +void InnerWidget::setPressed( + Row *pressed, + bool pressedTopicJump, + bool pressedBotApp) { + if ((_pressed != pressed) + || (pressed && _pressedTopicJump != pressedTopicJump) + || (pressed && _pressedBotApp != pressedBotApp)) { if (_pressed) { _pressed->stopLastRipple(); } + if (_pressedBotAppData && _pressedBotAppData->ripple) { + _pressedBotAppData->ripple->lastStop(); + } _pressed = pressed; - if (pressed || !pressedTopicJump) { + if (pressed || !pressedTopicJump || !pressedBotApp) { _pressedTopicJump = pressedTopicJump; + _pressedBotApp = pressedBotApp; + if (pressedBotApp) { + if (const auto user = MaybeBotWithApp(pressed)) { + const auto it = _rightButtons.find(user->id); + if (it != _rightButtons.end()) { + _pressedBotAppData = &(it->second); + } + } + } const auto history = pressedTopicJump ? pressed->history() : nullptr; @@ -2170,7 +2293,7 @@ void InnerWidget::setPressed(Row *pressed, bool pressedTopicJump) { } void InnerWidget::clearPressed() { - setPressed(nullptr, false); + setPressed(nullptr, false, false); } void InnerWidget::setHashtagPressed(int pressed) { @@ -2265,7 +2388,7 @@ void InnerWidget::dialogRowReplaced( _selected = newRow; } if (_pressed == oldRow) { - setPressed(newRow, _pressedTopicJump); + setPressed(newRow, _pressedTopicJump, _pressedBotApp); } if (_dragging == oldRow) { if (newRow) { @@ -4822,4 +4945,8 @@ bool InnerWidget::jumpToDialogRow(RowDescriptor to) { return _controller->jumpToChatListEntry(to); } +rpl::producer InnerWidget::openBotMainAppRequests() const { + return _openBotMainAppRequests.events(); +} + } // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 43fb5bc92..a9c9a84b7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -64,6 +64,7 @@ class SearchTags; class SearchEmpty; class ChatSearchIn; enum class HashOrCashtag : uchar; +struct RightButton; struct ChosenRow { Key key; @@ -200,6 +201,8 @@ public: return _touchCancelRequests.events(); } + [[nodiscard]] rpl::producer openBotMainAppRequests() const; + protected: void visibleTopBottomUpdated( int visibleTop, @@ -286,7 +289,7 @@ private: void scrollToItem(int top, int height); void scrollToDefaultSelected(); void setCollapsedPressed(int pressed); - void setPressed(Row *pressed, bool pressedTopicJump); + void setPressed(Row *pressed, bool pressedTopicJump, bool pressedBotApp); void clearPressed(); void setHashtagPressed(int pressed); void setFilteredPressed(int pressed, bool pressedTopicJump); @@ -451,6 +454,11 @@ private: void saveChatsFilterScrollState(FilterId filterId); void restoreChatsFilterScrollState(FilterId filterId); + [[nodiscard]] bool lookupIsInBotAppButton( + Row *row, + QPoint localPosition); + [[nodiscard]] RightButton *maybeCacheRightButton(Row *row); + [[nodiscard]] QImage *cacheChatsFilterTag( const Data::ChatFilter &filter, uint8 more, @@ -483,6 +491,10 @@ private: bool _selectedTopicJump = false; bool _pressedTopicJump = false; + RightButton *_pressedBotAppData = nullptr; + bool _selectedBotApp = false; + bool _pressedBotApp = false; + Row *_dragging = nullptr; int _draggingIndex = -1; int _aboveIndex = -1; @@ -566,6 +578,8 @@ private: bool _waitingAllChatListEntryRefreshesForTags = false; rpl::lifetime _handleChatListEntryTagRefreshesLifetime; + std::unordered_map _rightButtons; + Fn _loadMoreCallback; Fn _loadMoreFilteredCallback; rpl::event_stream<> _listBottomReached; @@ -577,6 +591,7 @@ private: rpl::event_stream _searchRequests; rpl::event_stream _completeHashtagRequests; rpl::event_stream<> _refreshHashtagsRequests; + rpl::event_stream _openBotMainAppRequests; RowDescriptor _chatPreviewRow; bool _chatPreviewScheduled = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 7c737d4b5..bc5841380 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -478,6 +478,12 @@ Widget::Widget( ) | rpl::start_with_next([=](const ChosenRow &row) { chosenRow(row); }, lifetime()); + _inner->openBotMainAppRequests( + ) | rpl::start_with_next([=](UserId userId) { + if (const auto user = session().data().user(userId)) { + openBotMainApp(user); + } + }, lifetime()); _scroll->geometryChanged( ) | rpl::start_with_next(crl::guard(_inner, [=] { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 1a7b5a1ac..f78d9dc09 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -7,41 +7,42 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "dialogs/ui/dialogs_layout.h" +#include "base/unixtime.h" +#include "core/ui_integration.h" +#include "data/data_channel.h" #include "data/data_drafts.h" +#include "data/data_folder.h" #include "data/data_forum_topic.h" +#include "data/data_peer_values.h" #include "data/data_saved_sublist.h" #include "data/data_session.h" +#include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "dialogs/dialogs_list.h" #include "dialogs/dialogs_three_state_icon.h" #include "dialogs/ui/dialogs_video_userpic.h" -#include "styles/style_dialogs.h" -#include "styles/style_window.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_components.h" +#include "history/history_item_helpers.h" +#include "history/history_unread_things.h" +#include "history/view/history_view_item_preview.h" +#include "history/view/history_view_send_action.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" #include "storage/localstorage.h" +#include "support/support_helper.h" #include "ui/empty_userpic.h" +#include "ui/painter.h" +#include "ui/power_saving.h" #include "ui/text/format_values.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" #include "ui/unread_badge.h" #include "ui/unread_badge_paint.h" -#include "ui/painter.h" -#include "ui/power_saving.h" -#include "core/ui_integration.h" -#include "lang/lang_keys.h" -#include "support/support_helper.h" -#include "main/main_session.h" -#include "history/view/history_view_send_action.h" -#include "history/view/history_view_item_preview.h" -#include "history/history_unread_things.h" -#include "history/history_item.h" -#include "history/history_item_components.h" -#include "history/history_item_helpers.h" -#include "history/history.h" -#include "base/unixtime.h" -#include "data/data_channel.h" -#include "data/data_user.h" -#include "data/data_folder.h" -#include "data/data_peer_values.h" +#include "styles/style_dialogs.h" +#include "styles/style_widgets.h" +#include "styles/style_window.h" namespace Dialogs::Ui { namespace { @@ -84,6 +85,55 @@ void PaintRowTopRight( text); } +int PaintRightButton(QPainter &p, const PaintContext &context) { + if (context.width < st::columnMinimalWidthLeft) { + return 0; + } + if (const auto rightButton = context.rightButton) { + const auto size = rightButton->bg.size() / style::DevicePixelRatio(); + const auto left = context.width + - size.width() + - st::dialogRowOpenBotRight; + const auto top = st::dialogRowOpenBotTop; + p.drawImage( + left, + top, + context.active + ? rightButton->activeBg + : context.selected + ? rightButton->selectedBg + : rightButton->bg); + if (rightButton->ripple) { + rightButton->ripple->paint( + p, + left, + top, + size.width() - size.height() / 2, + context.active + ? &st::universalRippleAnimation.color->c + : &st::activeButtonBgRipple->c); + if (rightButton->ripple->empty()) { + rightButton->ripple.reset(); + } + } + p.setPen(context.active + ? st::activeButtonBg + : context.selected + ? st::activeButtonFgOver + : st::activeButtonFg); + rightButton->text.draw(p, { + .position = QPoint( + left + size.height() / 2, + top + (st::dialogRowOpenBotHeight - rightButton->text.minHeight()) / 2), + .availableWidth = size.width() - size.height() / 2, + .outerWidth = size.width() - size.height() / 2, + .elisionLines = 1, + }); + return size.width() + st::dialogsUnreadPadding; + } + return 0; +} + int PaintBadges( QPainter &p, const PaintContext &context, @@ -93,7 +143,9 @@ int PaintBadges( bool displayPinnedIcon = false, int pinnedIconTop = 0) { auto initial = right; - if (badgesState.unread + if (const auto used = PaintRightButton(p, context)) { + return used - st::dialogsUnreadPadding; + } else if (badgesState.unread && !badgesState.unreadCounter && context.st->unreadMarkDiameter > 0) { const auto d = context.st->unreadMarkDiameter; @@ -430,7 +482,9 @@ void PaintRow( } auto availableWidth = namewidth; - if (entry->isPinnedDialog(context.filter) + if (const auto used = PaintRightButton(p, context)) { + availableWidth -= used; + } else if (entry->isPinnedDialog(context.filter) && (context.filter || !entry->fixedOnTopIndex())) { auto &icon = ThreeStateIcon( st::dialogsPinnedIcon, @@ -528,7 +582,9 @@ void PaintRow( } } else if (!item) { auto availableWidth = namewidth; - if (entry->isPinnedDialog(context.filter) + if (const auto used = PaintRightButton(p, context)) { + availableWidth -= used; + } else if (entry->isPinnedDialog(context.filter) && (context.filter || !entry->fixedOnTopIndex())) { auto &icon = ThreeStateIcon( st::dialogsPinnedIcon, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h index 5e51c8e73..914052447 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h @@ -29,6 +29,7 @@ namespace Dialogs { class Row; class FakeRow; class BasicRow; +struct RightButton; } // namespace Dialogs namespace Dialogs::Ui { @@ -53,6 +54,7 @@ struct TopicJumpCache { }; struct PaintContext { + RightButton *rightButton = nullptr; std::vector *chatsFilterTags = nullptr; not_null st; TopicJumpCache *topicJumpCache = nullptr;