diff --git a/Telegram/SourceFiles/core/local_url_handlers.cpp b/Telegram/SourceFiles/core/local_url_handlers.cpp index adc083d79..8c5b3b7d2 100644 --- a/Telegram/SourceFiles/core/local_url_handlers.cpp +++ b/Telegram/SourceFiles/core/local_url_handlers.cpp @@ -69,6 +69,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "main/main_domain.h" #include "main/main_session.h" #include "main/main_session_settings.h" +#include "info/info_controller.h" +#include "info/info_memento.h" #include "inline_bots/bot_attach_web_view.h" #include "history/history.h" #include "history/history_item.h" @@ -1034,6 +1036,26 @@ bool EditPaidMessagesFee( return true; } +bool ShowCommonGroups( + Window::SessionController *controller, + const Match &match, + const QVariant &context) { + if (!controller) { + return false; + } + const auto peerId = PeerId(match->captured(1).toULongLong()); + if (const auto id = peerToUser(peerId)) { + const auto user = controller->session().data().userLoaded(id); + if (user) { + controller->showSection( + std::make_shared( + user, + Info::Section::Type::CommonGroups)); + } + } + return true; +} + bool ShowStarsExamples( Window::SessionController *controller, const Match &match, @@ -1541,6 +1563,10 @@ const std::vector &InternalUrlHandlers() { u"^edit_paid_messages_fee/([0-9]+)$"_q, EditPaidMessagesFee, }, + { + u"^common_groups/([0-9]+)$"_q, + ShowCommonGroups, + }, { u"^stars_examples$"_q, ShowStarsExamples, diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 983ec2839..362d5ef8b 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -4342,6 +4342,9 @@ void HistoryInner::refreshAboutView(bool force) { _aboutView = std::make_unique( _history, _history->delegateMixin()->delegate()); + _aboutView->refreshRequests() | rpl::start_with_next([=] { + updateBotInfo(); + }, _aboutView->lifetime()); } }; if (const auto user = _peer->asUser()) { diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.cpp b/Telegram/SourceFiles/history/view/history_view_about_view.cpp index ca6eb9a83..c1f9346b5 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.cpp +++ b/Telegram/SourceFiles/history/view/history_view_about_view.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_document.h" #include "data/data_session.h" #include "data/data_user.h" +#include "history/view/history_view_group_call_bar.h" #include "history/view/media/history_view_media_generic.h" #include "history/view/media/history_view_service_box.h" #include "history/view/media/history_view_sticker_player_abstract.h" @@ -37,17 +38,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "settings/settings_credits.h" // BuyStarsHandler #include "settings/settings_premium.h" #include "ui/chat/chat_style.h" +#include "ui/text/custom_emoji_instance.h" #include "ui/text/text_utilities.h" #include "ui/text/text_options.h" +#include "ui/dynamic_image.h" #include "ui/painter.h" #include "window/window_session_controller.h" #include "styles/style_chat.h" +#include "styles/style_chat_helpers.h" // GroupCallUserpics #include "styles/style_credits.h" namespace HistoryView { namespace { constexpr auto kLabelOpacity = 0.85; +constexpr auto kMaxCommonChatsUserpics = 3; class EmptyChatLockedBox final : public ServiceBoxContent @@ -95,6 +100,101 @@ private: }; +class UserpicsList final : public Ui::DynamicImage { +public: + UserpicsList( + std::vector> peers, + const style::GroupCallUserpics &st, + int countOverride = 0); + + [[nodiscard]] int width() const; + + std::shared_ptr clone() override; + + QImage image(int size) override; + void subscribeToUpdates(Fn callback) override; + +private: + struct Subscribed { + explicit Subscribed(Fn callback) + : callback(std::move(callback)) { + } + + std::vector list; + bool someNotLoaded = false; + Fn callback; + int paletteVersion = 0; + }; + + const std::vector> _peers; + const style::GroupCallUserpics &_st; + const int _countOverride = 0; + + QImage _frame; + std::unique_ptr _subscribed; + +}; + +UserpicsList::UserpicsList( + std::vector> peers, + const style::GroupCallUserpics &st, + int countOverride) +: _peers(std::move(peers)) +, _st(st) +, _countOverride(countOverride) { +} + +std::shared_ptr UserpicsList::clone() { + return std::make_shared(_peers, _st); +} + +QImage UserpicsList::image(int size) { + Expects(_subscribed != nullptr); + + const auto regenerate = [&] { + const auto version = style::PaletteVersion(); + if (_frame.isNull() || _subscribed->paletteVersion != version) { + _subscribed->paletteVersion = version; + return true; + } + for (auto &entry : _subscribed->list) { + const auto peer = entry.peer; + auto &view = entry.view; + const auto wasView = view.cloud.get(); + if (peer->userpicUniqueKey(view) != entry.uniqueKey + || view.cloud.get() != wasView) { + return true; + } + } + return false; + }(); + if (regenerate) { + const auto max = std::max(_countOverride, int(_peers.size())); + GenerateUserpicsInRow(_frame, _subscribed->list, _st, max); + } + return _frame; +} + +void UserpicsList::subscribeToUpdates(Fn callback) { + if (!callback) { + _subscribed = nullptr; + return; + } + _subscribed = std::make_unique(std::move(callback)); + for (const auto peer : _peers) { + _subscribed->list.push_back({ .peer = peer }); + } +} + +int UserpicsList::width() const { + const auto count = std::max(_countOverride, int(_peers.size())); + if (!count) { + return 0; + } + const auto shifted = count - 1; + return _st.size + (shifted * (_st.size - _st.shift)); +} + auto GenerateChatIntro( not_null parent, Element *replacing, @@ -165,7 +265,8 @@ auto GenerateChatIntro( auto GenerateNewPeerInfo( not_null parent, Element *replacing, - not_null user) + not_null user, + std::vector> commonGroups) -> Fn, Fn)>)> { @@ -208,9 +309,22 @@ auto GenerateNewPeerInfo( }); } + const auto context = Core::TextContext({ + .session = &parent->history()->session(), + .repaint = [parent] { parent->repaint(); }, + }); + const auto kUserpicsPrefix = u"userpics-list/"_q; if (const auto count = user->commonChatsCount()) { const auto url = u"internal:common_groups/"_q + QString::number(user->id.value); + auto ids = QStringList(); + const auto userpics = std::min(count, kMaxCommonChatsUserpics); + for (auto i = 0; i != userpics; ++i) { + ids.push_back(QString::number(i < commonGroups.size() + ? commonGroups[i]->id.value + : 0)); + } + auto userpicsData = kUserpicsPrefix + ids.join(','); entries.push_back({ tr::lng_new_contact_common_groups(tr::now), Ui::Text::Wrapped( @@ -219,7 +333,7 @@ auto GenerateNewPeerInfo( lt_count, count, lt_emoji, - TextWithEntities(), + Ui::Text::SingleCustomEmoji(userpicsData), lt_arrow, Ui::Text::IconEmoji(&st::textMoreIconEmoji), Ui::Text::Bold), @@ -228,16 +342,40 @@ auto GenerateNewPeerInfo( }); } + auto copy = context; + copy.customEmojiFactory = [=, old = copy.customEmojiFactory]( + QStringView data, + const Ui::Text::MarkedContext &context + ) -> std::unique_ptr { + if (!data.startsWith(kUserpicsPrefix)) { + return old(data, context); + } + const auto ids = data.mid(kUserpicsPrefix.size()).split(','); + auto peers = std::vector>(); + for (const auto &id : ids) { + if (const auto peerId = PeerId(id.toULongLong())) { + peers.push_back(user->owner().peer(peerId)); + } + } + auto image = std::make_shared( + std::move(peers), + st::newPeerUserpics, + ids.size()); + const auto size = image->width(); + return std::make_unique( + data.toString(), + std::move(image), + context.repaint, + st::newPeerUserpicsPadding, + size); + }; push(std::make_unique( std::move(entries), st::newPeerSubtitleMargin, fadedFg, - normalFg)); + normalFg, + copy)); - const auto context = Core::TextContext({ - .session = &parent->history()->session(), - .repaint = [parent] { parent->repaint(); }, - }); const auto details = user->botVerifyDetails(); const auto text = details ? Data::SingleCustomEmoji( @@ -380,9 +518,10 @@ bool AboutView::refresh() { if (user && !user->isContact() && !user->phoneCountryCode().isEmpty()) { - if (_item) { + if (_item && !_commonGroupsStale) { return false; } + loadCommonGroups(); setItem(makeNewPeerInfo(user), nullptr); return true; } else if (user && !user->isSelf() && _history->isDisplayedEmpty()) { @@ -474,6 +613,14 @@ void AboutView::make(Data::ChatIntro data, bool preview) { setItem(std::move(owned), data.sticker); } +rpl::producer<> AboutView::refreshRequests() const { + return _refreshRequests.events(); +} + +rpl::lifetime &AboutView::lifetime() { + return _lifetime; +} + void AboutView::toggleStickerRegistered(bool registered) { if (const auto item = _item ? _item->data().get() : nullptr) { if (_sticker) { @@ -490,6 +637,75 @@ void AboutView::toggleStickerRegistered(bool registered) { } } +void AboutView::loadCommonGroups() { + if (_commonGroupsRequested) { + return; + } + _commonGroupsRequested = true; + + const auto user = _history->peer->asUser(); + if (!user) { + return; + } + + struct Cached { + std::vector> list; + }; + struct Session { + base::flat_map, Cached> data; + }; + static auto Map = base::flat_map, Session>(); + const auto session = &_history->session(); + auto i = Map.find(session); + if (i == end(Map)) { + i = Map.emplace(session).first; + session->lifetime().add([session] { + Map.remove(session); + }); + } + auto &cached = i->second.data[user]; + + const auto count = user->commonChatsCount(); + if (!count) { + cached = {}; + return; + } else while (cached.list.size() > count) { + cached.list.pop_back(); + } + _commonGroups = cached.list; + const auto requestId = _history->session().api().request( + MTPmessages_GetCommonChats( + user->inputUser, + MTP_long(0), + MTP_int(kMaxCommonChatsUserpics)) + ).done([=](const MTPmessages_Chats &result) { + const auto chats = result.match([](const auto &data) { + return &data.vchats().v; + }); + auto &owner = user->session().data(); + auto list = std::vector>(); + list.reserve(chats->size()); + for (const auto &chat : *chats) { + if (const auto peer = owner.processChat(chat)) { + list.push_back(peer); + if (list.size() == kMaxCommonChatsUserpics) { + break; + } + } + } + if (_commonGroups != list) { + Map[session].data[user].list = list; + _commonGroups = std::move(list); + _commonGroupsStale = true; + _refreshRequests.fire({}); + } + }).send(); + + _lifetime.add([=] { + _history->session().api().request(requestId).cancel(); + }); +} + void AboutView::setHelloChosen(not_null sticker) { _helloChosen = sticker; toggleStickerRegistered(false); @@ -505,6 +721,8 @@ void AboutView::setItem(AdminLog::OwnedItem item, DocumentData *sticker) { } AdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null user) { + _commonGroupsStale = false; + const auto text = user->name(); const auto item = _history->makeMessage({ .id = _history->nextNonHistoryEntryId(), @@ -517,7 +735,7 @@ AdminLog::OwnedItem AboutView::makeNewPeerInfo(not_null user) { auto owned = AdminLog::OwnedItem(_delegate, item); owned->overrideMedia(std::make_unique( owned.get(), - GenerateNewPeerInfo(owned.get(), _item.get(), user), + GenerateNewPeerInfo(owned.get(), _item.get(), user, _commonGroups), HistoryView::MediaGenericDescriptor{ .service = true, .hideServiceText = true, diff --git a/Telegram/SourceFiles/history/view/history_view_about_view.h b/Telegram/SourceFiles/history/view/history_view_about_view.h index 2b304d92b..b52d7edf0 100644 --- a/Telegram/SourceFiles/history/view/history_view_about_view.h +++ b/Telegram/SourceFiles/history/view/history_view_about_view.h @@ -30,6 +30,9 @@ public: void make(Data::ChatIntro data, bool preview = false); + [[nodiscard]] rpl::producer<> refreshRequests() const; + [[nodiscard]] rpl::lifetime &lifetime(); + int top = 0; int height = 0; @@ -50,13 +53,22 @@ private: void setHelloChosen(not_null sticker); void toggleStickerRegistered(bool registered); + void loadCommonGroups(); + const not_null _history; const not_null _delegate; AdminLog::OwnedItem _item; + DocumentData *_helloChosen = nullptr; DocumentData *_sticker = nullptr; int _version = 0; + bool _commonGroupsStale = false; + bool _commonGroupsRequested = false; + std::vector> _commonGroups; + rpl::event_stream<> _refreshRequests; + rpl::lifetime _lifetime; + }; } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp index 287fbc9f1..abda42b78 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.cpp @@ -511,6 +511,26 @@ void AttributeTable::draw( } } +TextState AttributeTable::textState( + QPoint point, + StateRequest request, + int outerWidth) const { + auto top = _margins.top(); + for (const auto &part : _parts) { + const auto height = st::normalFont->height + st::chatUniqueRowSkip; + if (point.y() >= top && point.y() < top + height) { + point -= QPoint((outerWidth - width()) / 2 + _valueLeft, top); + auto result = TextState(); + auto forText = request.forText(); + forText.align = style::al_topleft; + result.link = part.value.getState(point, width(), forText).link; + return result; + } + top += height; + } + return {}; +} + QSize AttributeTable::countOptimalSize() { auto maxLabel = 0; auto maxValue = 0; diff --git a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h index 3110a618e..4816b1f68 100644 --- a/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h +++ b/Telegram/SourceFiles/history/view/media/history_view_unique_gift.h @@ -97,6 +97,10 @@ public: not_null owner, const PaintContext &context, int outerWidth) const override; + TextState textState( + QPoint point, + StateRequest request, + int outerWidth) const override; QSize countOptimalSize() override; QSize countCurrentSize(int newWidth) override; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index b657a31ad..eba8783e0 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -1237,3 +1237,10 @@ newPeerNonOfficial: IconEmoji { icon: icon{{ "chat/mini_info_alert", windowFg }}; padding: margins(0px, 2px, 0px, 0px); } +newPeerUserpics: GroupCallUserpics { + size: 16px; + shift: 5px; + stroke: 1px; + align: align(left); +} +newPeerUserpicsPadding: margins(0px, 3px, 0px, 0px); diff --git a/Telegram/SourceFiles/ui/unread_badge.cpp b/Telegram/SourceFiles/ui/unread_badge.cpp index 233ff51a9..d53f617da 100644 --- a/Telegram/SourceFiles/ui/unread_badge.cpp +++ b/Telegram/SourceFiles/ui/unread_badge.cpp @@ -296,9 +296,10 @@ void PeerBadge::set( _botVerifiedData = std::make_unique(); } if (details->iconId) { - _botVerifiedData->icon = factory( - Data::SerializeCustomEmojiId(details->iconId), - { .repaint = repaint }); + _botVerifiedData->icon = std::make_unique( + factory( + Data::SerializeCustomEmojiId(details->iconId), + { .repaint = repaint })); } } diff --git a/Telegram/lib_ui b/Telegram/lib_ui index e51fe382b..b9ef54ad5 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit e51fe382bda9f2944412078da70f04de5dd821f3 +Subproject commit b9ef54ad5eb6932930244f30a00ac9169cd09a46