diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index baf77c58c..0ffd19f99 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -460,10 +460,14 @@ PRIVATE data/business/data_business_info.h data/business/data_shortcut_messages.cpp data/business/data_shortcut_messages.h + data/components/recent_peers.cpp + data/components/recent_peers.h data/components/scheduled_messages.cpp data/components/scheduled_messages.h data/components/sponsored_messages.cpp data/components/sponsored_messages.h + data/components/top_peers.cpp + data/components/top_peers.h data/notify/data_notify_settings.cpp data/notify/data_notify_settings.h data/notify/data_peer_notify_settings.cpp @@ -608,6 +612,18 @@ PRIVATE data/data_wall_paper.h data/data_web_page.cpp data/data_web_page.h + dialogs/ui/dialogs_layout.cpp + dialogs/ui/dialogs_layout.h + dialogs/ui/dialogs_message_view.cpp + dialogs/ui/dialogs_message_view.h + dialogs/ui/dialogs_stories_content.cpp + dialogs/ui/dialogs_stories_content.h + dialogs/ui/dialogs_suggestions.cpp + dialogs/ui/dialogs_suggestions.h + dialogs/ui/dialogs_topics_view.cpp + dialogs/ui/dialogs_topics_view.h + dialogs/ui/dialogs_video_userpic.cpp + dialogs/ui/dialogs_video_userpic.h dialogs/dialogs_entry.cpp dialogs/dialogs_entry.h dialogs/dialogs_indexed_list.cpp @@ -630,16 +646,6 @@ PRIVATE dialogs/dialogs_search_tags.h dialogs/dialogs_widget.cpp dialogs/dialogs_widget.h - dialogs/ui/dialogs_layout.cpp - dialogs/ui/dialogs_layout.h - dialogs/ui/dialogs_message_view.cpp - dialogs/ui/dialogs_message_view.h - dialogs/ui/dialogs_stories_content.cpp - dialogs/ui/dialogs_stories_content.h - dialogs/ui/dialogs_topics_view.cpp - dialogs/ui/dialogs_topics_view.h - dialogs/ui/dialogs_video_userpic.cpp - dialogs/ui/dialogs_video_userpic.h editor/color_picker.cpp editor/color_picker.h editor/controllers/controllers.h diff --git a/Telegram/SourceFiles/api/api_updates.cpp b/Telegram/SourceFiles/api/api_updates.cpp index 9918ec7a9..314e6c862 100644 --- a/Telegram/SourceFiles/api/api_updates.cpp +++ b/Telegram/SourceFiles/api/api_updates.cpp @@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_dc_options.h" #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" +#include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_saved_messages.h" @@ -1577,6 +1578,11 @@ void Updates::feedUpdate(const MTPUpdate &update) { } else { if (existing) { existing->destroy(); + } else { + // Not the server-side date, but close enough. + session().topPeers().increment( + local->history()->peer, + local->date()); } local->setRealId(d.vid().v); } diff --git a/Telegram/SourceFiles/data/components/recent_peers.cpp b/Telegram/SourceFiles/data/components/recent_peers.cpp new file mode 100644 index 000000000..32fc0ad42 --- /dev/null +++ b/Telegram/SourceFiles/data/components/recent_peers.cpp @@ -0,0 +1,18 @@ +/* +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 "data/components/recent_peers.h" + +namespace Data { + +RecentPeers::RecentPeers(not_null session) +: _session(session) { +} + +RecentPeers::~RecentPeers() = default; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/recent_peers.h b/Telegram/SourceFiles/data/components/recent_peers.h new file mode 100644 index 000000000..e3a648b47 --- /dev/null +++ b/Telegram/SourceFiles/data/components/recent_peers.h @@ -0,0 +1,26 @@ +/* +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 + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class RecentPeers final { +public: + explicit RecentPeers(not_null session); + ~RecentPeers(); + +private: + const not_null _session; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/top_peers.cpp b/Telegram/SourceFiles/data/components/top_peers.cpp new file mode 100644 index 000000000..253c716f3 --- /dev/null +++ b/Telegram/SourceFiles/data/components/top_peers.cpp @@ -0,0 +1,157 @@ +/* +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 "data/components/top_peers.h" + +#include "api/api_hash.h" +#include "apiwrap.h" +#include "data/data_peer.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "main/main_session.h" +#include "mtproto/mtproto_config.h" + +namespace Data { +namespace { + +constexpr auto kLimit = 32; +constexpr auto kRequestTimeLimit = 10 * crl::time(1000); + +[[nodiscard]] float64 RatingDelta(TimeId now, TimeId was, int decay) { + return std::exp((now - was) * 1. / decay); +} + +} // namespace + +TopPeers::TopPeers(not_null session) +: _session(session) { + using namespace rpl::mappers; + crl::on_main(session, [=] { + _session->data().chatsListLoadedEvents( + ) | rpl::filter(_1 == nullptr) | rpl::start_with_next([=] { + crl::on_main(_session, [=] { + request(); + }); + }, _session->lifetime()); + }); +} + +TopPeers::~TopPeers() = default; + +std::vector> TopPeers::list() const { + return _list + | ranges::view::transform(&TopPeer::peer) + | ranges::to_vector; +} + +bool TopPeers::disabled() const { + return _disabled; +} + +rpl::producer<> TopPeers::updates() const { + return _updates.events(); +} + +void TopPeers::increment(not_null peer, TimeId date) { + if (date <= _lastReceivedDate) { + return; + } + if (const auto user = peer->asUser(); user && !user->isBot()) { + auto changed = false; + auto i = ranges::find(_list, peer, &TopPeer::peer); + if (i == end(_list)) { + _list.push_back({ .peer = peer }); + i = end(_list) - 1; + changed = true; + } + const auto &config = peer->session().mtp().config(); + const auto decay = config.values().ratingDecay; + i->rating += RatingDelta(date, _lastReceivedDate, decay); + for (; i != begin(_list); --i) { + if (i->rating >= (i - 1)->rating) { + changed = true; + std::swap(*i, *(i - 1)); + } else { + break; + } + } + if (changed) { + _updates.fire({}); + } + } +} + +void TopPeers::reload() { + if (_requestId + || (_lastReceived + && _lastReceived + kRequestTimeLimit > crl::now())) { + return; + } + request(); +} + +void TopPeers::request() { + if (_requestId) { + return; + } + + _requestId = _session->api().request(MTPcontacts_GetTopPeers( + MTP_flags(MTPcontacts_GetTopPeers::Flag::f_correspondents), + MTP_int(0), + MTP_int(kLimit), + MTP_long(countHash()) + )).done([=](const MTPcontacts_TopPeers &result, const MTP::Response &response) { + _lastReceivedDate = TimeId(response.outerMsgId >> 32); + _lastReceived = crl::now(); + _requestId = 0; + + result.match([&](const MTPDcontacts_topPeers &data) { + _disabled = false; + const auto owner = &_session->data(); + owner->processUsers(data.vusers()); + owner->processChats(data.vchats()); + for (const auto &category : data.vcategories().v) { + const auto &data = category.data(); + data.vcategory().match( + [&](const MTPDtopPeerCategoryCorrespondents &) { + _list = ranges::views::all( + data.vpeers().v + ) | ranges::views::transform([&](const MTPTopPeer &top) { + return TopPeer{ + owner->peer(peerFromMTP(top.data().vpeer())), + top.data().vrating().v, + }; + }) | ranges::to_vector; + }, [](const auto &) { + LOG(("API Error: Unexpected top peer category.")); + }); + } + _updates.fire({}); + }, [&](const MTPDcontacts_topPeersDisabled &) { + if (!_disabled) { + _list.clear(); + _disabled = true; + _updates.fire({}); + } + }, [](const MTPDcontacts_topPeersNotModified &) { + }); + }).fail([=] { + _lastReceived = crl::now(); + _requestId = 0; + }).send(); +} + +uint64 TopPeers::countHash() const { + using namespace Api; + auto hash = HashInit(); + for (const auto &top : _list) { + HashUpdate(hash, peerToUser(top.peer->id).bare); + } + return HashFinalize(hash); +} + +} // namespace Data diff --git a/Telegram/SourceFiles/data/components/top_peers.h b/Telegram/SourceFiles/data/components/top_peers.h new file mode 100644 index 000000000..b5107381c --- /dev/null +++ b/Telegram/SourceFiles/data/components/top_peers.h @@ -0,0 +1,51 @@ +/* +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 + +namespace Main { +class Session; +} // namespace Main + +namespace Data { + +class TopPeers final { +public: + explicit TopPeers(not_null session); + ~TopPeers(); + + [[nodiscard]] std::vector> list() const; + [[nodiscard]] bool disabled() const; + [[nodiscard]] rpl::producer<> updates() const; + + void increment(not_null peer, TimeId date); + void reload(); + +private: + struct TopPeer { + not_null peer; + float64 rating = 0.; + }; + + void request(); + [[nodiscard]] uint64 countHash() const; + + const not_null _session; + + std::vector _list; + rpl::event_stream<> _updates; + crl::time _lastReceived = 0; + TimeId _lastReceivedDate = 0; + + mtpRequestId _requestId = 0; + + bool _disabled = false; + bool _received = false; + +}; + +} // namespace Data diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 224417f20..d0d340b09 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -591,6 +591,11 @@ dialogsStoriesFull: DialogsStories { font: font(11px); } } +topPeers: DialogsStories(dialogsStoriesFull) { + photo: 46px; + photoLeft: 10px; + photoTop: 8px; +} dialogsStoriesList: DialogsStoriesList { small: dialogsStories; @@ -633,3 +638,4 @@ dialogsSearchTagArrowPadding: margins(-6px, 3px, 0px, 0px); dialogsSearchTagPromoLeft: 6px; dialogsSearchTagPromoRight: 1px; dialogsSearchTagPromoSkip: 6px; + diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index e0afa642e..f60781b44 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/options.h" #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_list.h" +#include "dialogs/ui/dialogs_suggestions.h" #include "dialogs/dialogs_inner_widget.h" #include "dialogs/dialogs_search_from_controllers.h" #include "dialogs/dialogs_key.h" @@ -1021,7 +1022,10 @@ void Widget::fullSearchRefreshOn(rpl::producer<> events) { void Widget::updateControlsVisibility(bool fast) { updateLoadMoreChatsVisibility(); - _scroll->show(); + _scroll->setVisible(!_suggestions); + if (_suggestions) { + _suggestions->show(); + } updateStoriesVisibility(); if ((_openedFolder || _openedForum) && _searchHasFocus.current()) { setInnerFocus(); @@ -1085,8 +1089,36 @@ void Widget::updateLockUnlockPosition() { } void Widget::updateHasFocus(not_null focused) { - _searchHasFocus = (focused == _search.data()); - updateForceDisplayWide(); + const auto has = (focused == _search.data()); + if (_searchHasFocus.current() != has) { + _searchHasFocus = (focused == _search.data()); + updateStoriesVisibility(); + updateForceDisplayWide(); + updateSuggestions(anim::type::normal); + } +} + +void Widget::updateSuggestions(anim::type animated) { + const auto suggest = _searchHasFocus.current() + && !_searchInChat + && (_inner->state() == WidgetState::Default); + if (!suggest && _suggestions) { + _suggestions = nullptr; + _scroll->show(); + } else if (suggest && !_suggestions) { + _suggestions = std::make_unique( + this, + rpl::single(TopPeersContent(&session()))); + + _suggestions->topPeerChosen( + ) | rpl::start_with_next([=](PeerId id) { + controller()->showPeerHistory(id); + }, _suggestions->lifetime()); + + _suggestions->show(); + _scroll->hide(); + updateControlsGeometry(); + } } void Widget::changeOpenedSubsection( @@ -1513,7 +1545,10 @@ void Widget::startWidthAnimation() { void Widget::stopWidthAnimation() { _widthAnimationCache = QPixmap(); if (!_showAnimation) { - _scroll->show(); + _scroll->setVisible(!_suggestions); + if (_suggestions) { + _suggestions->show(); + } } updateStoriesVisibility(); update(); @@ -1528,6 +1563,7 @@ void Widget::updateStoriesVisibility() { || _openedForum || !_widthAnimationCache.isNull() || _childList + || _searchHasFocus.current() || !_search->getLastText().isEmpty() || _searchInChat || _stories->empty(); @@ -2460,6 +2496,7 @@ void Widget::applySearchUpdate(bool force) { clearSearchCache(); } _cancelSearch->toggle(!filterText.isEmpty(), anim::type::normal); + updateSuggestions(anim::type::instant); updateLoadMoreChatsVisibility(); updateJumpToDateVisibility(); updateLockUnlockPosition(); @@ -2675,6 +2712,7 @@ bool Widget::setSearchInChat( if (searchInPeerUpdated) { _searchInChat = chat; controller()->setSearchInChat(_searchInChat); + updateSuggestions(anim::type::instant); updateJumpToDateVisibility(); updateStoriesVisibility(); } @@ -3041,6 +3079,14 @@ void Widget::updateControlsGeometry() { }; _updateScrollGeometryCached(); + if (_suggestions) { + _suggestions->setGeometry( + 0, + expandedStoriesTop, + scrollWidth, + height() - expandedStoriesTop - bottomSkip); + } + _inner->resize(scrollWidth, _inner->height()); _inner->setNarrowRatio(narrowRatio); if (newScrollTop != wasScrollTop) { @@ -3097,6 +3143,7 @@ void Widget::keyPressEvent(QKeyEvent *e) { && !_openedFolder && !_openedForum && _search->isVisible() + && !_search->hasFocus() && !e->text().isEmpty()) { _search->setFocusFast(); QCoreApplication::sendEvent(_search->rawTextEdit(), e); diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.h b/Telegram/SourceFiles/dialogs/dialogs_widget.h index f3ebbd715..f3b54aa49 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.h @@ -75,6 +75,7 @@ class Key; struct ChosenRow; class InnerWidget; enum class SearchRequestType; +class Suggestions; class Widget final : public Window::AbstractSectionWidget { public: @@ -242,6 +243,7 @@ private: void startScrollUpButtonAnimation(bool shown); void updateScrollUpPosition(); void updateLockUnlockPosition(); + void updateSuggestions(anim::type animated); MTP::Sender _api; @@ -273,6 +275,7 @@ private: object_ptr _scroll; QPointer _inner; + std::unique_ptr _suggestions; class BottomButton; object_ptr _updateTelegram = { nullptr }; object_ptr _loadMoreChats = { nullptr }; @@ -291,7 +294,7 @@ private: Data::Folder *_openedFolder = nullptr; Data::Forum *_openedForum = nullptr; - Dialogs::Key _searchInChat; + Key _searchInChat; History *_searchInMigrated = nullptr; PeerData *_searchFromAuthor = nullptr; std::vector _searchTags; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index f615f4fb3..1e4a4cd5d 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -184,9 +184,9 @@ rpl::producer List::toggleExpandedRequests() const { return _toggleExpandedRequests.events(); } -rpl::producer<> List::entered() const { - return _entered.events(); -} +//rpl::producer<> List::entered() const { +// return _entered.events(); +//} rpl::producer<> List::loadMoreRequests() const { return _loadMoreRequests.events(); @@ -217,9 +217,9 @@ void List::requestExpanded(bool expanded) { _toggleExpandedRequests.fire_copy(_expanded); } -void List::enterEventHook(QEnterEvent *e) { - _entered.fire({}); -} +//void List::enterEventHook(QEnterEvent *e) { + //_entered.fire({}); +//} void List::resizeEvent(QResizeEvent *e) { updateScrollMax(); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 869767745..303938957 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -94,7 +94,7 @@ public: [[nodiscard]] rpl::producer clicks() const; [[nodiscard]] rpl::producer showMenuRequests() const; [[nodiscard]] rpl::producer toggleExpandedRequests() const; - [[nodiscard]] rpl::producer<> entered() const; + //[[nodiscard]] rpl::producer<> entered() const; [[nodiscard]] rpl::producer<> loadMoreRequests() const; [[nodiscard]] auto verticalScrollEvents() const @@ -123,7 +123,7 @@ private: }; void showContent(Content &&content); - void enterEventHook(QEnterEvent *e) override; + //void enterEventHook(QEnterEvent *e) override; void resizeEvent(QResizeEvent *e) override; void paintEvent(QPaintEvent *e) override; void wheelEvent(QWheelEvent *e) override; @@ -173,7 +173,7 @@ private: rpl::event_stream _clicks; rpl::event_stream _showMenuRequests; rpl::event_stream _toggleExpandedRequests; - rpl::event_stream<> _entered; + //rpl::event_stream<> _entered; rpl::event_stream<> _loadMoreRequests; rpl::event_stream<> _collapsedGeometryChanged; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp new file mode 100644 index 000000000..9c9e90bc4 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.cpp @@ -0,0 +1,77 @@ +/* +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 "dialogs/ui/dialogs_suggestions.h" + +#include "data/components/top_peers.h" +#include "data/data_user.h" +#include "main/main_session.h" +#include "ui/widgets/elastic_scroll.h" +#include "ui/widgets/labels.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/dynamic_thumbnails.h" +#include "styles/style_layers.h" + +namespace Dialogs { + +Suggestions::Suggestions( + not_null parent, + rpl::producer topPeers) +: RpWidget(parent) +, _scroll(std::make_unique(this)) +, _content(_scroll->setOwnedWidget(object_ptr(this))) +, _topPeersWrap(_content->add(object_ptr>( + this, + object_ptr(this, std::move(topPeers))))) +, _topPeers(_topPeersWrap->entity()) +, _divider(_content->add(setupDivider())) { + _topPeers->emptyValue() | rpl::start_with_next([=](bool empty) { + _topPeersWrap->toggle(!empty, anim::type::instant); + }, _topPeers->lifetime()); + + _topPeers->clicks() | rpl::start_with_next([=](uint64 peerIdRaw) { + _topPeerChosen.fire(PeerId(peerIdRaw)); + }, _topPeers->lifetime()); +} + +Suggestions::~Suggestions() = default; + +void Suggestions::paintEvent(QPaintEvent *e) { + QPainter(this).fillRect(e->rect(), st::windowBg); +} + +void Suggestions::resizeEvent(QResizeEvent *e) { + _scroll->setGeometry(rect()); + _content->resizeToWidth(width()); +} + +object_ptr Suggestions::setupDivider() { + auto result = object_ptr( + this, + object_ptr( + this, + rpl::single(u"Recent"_q), + st::boxDividerLabel), + st::defaultBoxDividerLabelPadding); + + return result; +} + +TopPeersList TopPeersContent(not_null session) { + auto result = TopPeersList(); + for (const auto &peer : session->topPeers().list()) { + result.entries.push_back(TopPeersEntry{ + .id = peer->id.value, + .name = peer->shortName(), + .userpic = Ui::MakeUserpicThumbnail(peer), + }); + } + return result; +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h new file mode 100644 index 000000000..10622576a --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_suggestions.h @@ -0,0 +1,59 @@ +/* +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/object_ptr.h" +#include "dialogs/ui/top_peers_strip.h" +#include "ui/rp_widget.h" + +namespace Main { +class Session; +} // namespace Main + +namespace Ui { +class ElasticScroll; +class VerticalLayout; +template +class SlideWrap; +} // namespace Ui + +namespace Dialogs { + +class Suggestions final : public Ui::RpWidget { +public: + Suggestions( + not_null parent, + rpl::producer topPeers); + ~Suggestions(); + + [[nodiscard]] rpl::producer topPeerChosen() const { + return _topPeerChosen.events(); + } + +private: + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + [[nodiscard]] object_ptr setupDivider(); + + void updateControlsGeometry(); + + const std::unique_ptr _scroll; + const not_null _content; + const not_null*> _topPeersWrap; + const not_null _topPeers; + const not_null _divider; + + rpl::event_stream _topPeerChosen; + +}; + +[[nodiscard]] TopPeersList TopPeersContent( + not_null session); + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp new file mode 100644 index 000000000..65adee797 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.cpp @@ -0,0 +1,302 @@ +/* +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 "dialogs/ui/top_peers_strip.h" + +#include "ui/text/text.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" +#include "ui/widgets/popup_menu.h" +#include "ui/dynamic_image.h" +#include "ui/painter.h" +#include "styles/style_dialogs.h" +#include "styles/style_widgets.h" + +#include + +namespace Dialogs { + +struct TopPeersStrip::Entry { + uint64 id = 0; + Ui::Text::String name; + std::shared_ptr userpic; + bool subscribed = false; +}; + +TopPeersStrip::TopPeersStrip( + not_null parent, + rpl::producer content) +: RpWidget(parent) { + resize(0, st::topPeers.height); + + std::move(content) | rpl::start_with_next([=](const TopPeersList &list) { + apply(list); + }, lifetime()); + + setMouseTracking(true); +} + +TopPeersStrip::~TopPeersStrip() = default; + +void TopPeersStrip::resizeEvent(QResizeEvent *e) { + updateScrollMax(); +} + +void TopPeersStrip::wheelEvent(QWheelEvent *e) { + const auto phase = e->phase(); + const auto fullDelta = e->pixelDelta().isNull() + ? e->angleDelta() + : e->pixelDelta(); + if (phase == Qt::ScrollBegin || phase == Qt::ScrollEnd) { + _scrollingLock = Qt::Orientation(); + if (fullDelta.isNull()) { + return; + } + } + const auto vertical = qAbs(fullDelta.x()) < qAbs(fullDelta.y()); + if (_scrollingLock == Qt::Orientation() && phase != Qt::NoScrollPhase) { + _scrollingLock = vertical ? Qt::Vertical : Qt::Horizontal; + } + if (_scrollingLock == Qt::Vertical || (vertical && !_scrollLeftMax)) { + _verticalScrollEvents.fire(e); + return; + } + const auto delta = vertical + ? fullDelta.y() + : ((style::RightToLeft() ? -1 : 1) * fullDelta.x()); + + const auto now = _scrollLeft; + const auto used = now - delta; + const auto next = std::clamp(used, 0, _scrollLeftMax); + if (next != now) { + _scrollLeft = next; + updateSelected(); + update(); + } + e->accept(); +} + +void TopPeersStrip::mousePressEvent(QMouseEvent *e) { + if (e->button() != Qt::LeftButton) { + return; + } + _lastMousePosition = e->globalPos(); + updateSelected(); + + _mouseDownPosition = _lastMousePosition; + _pressed = _selected; +} + +void TopPeersStrip::mouseMoveEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + updateSelected(); + + if (!_dragging && _mouseDownPosition) { + if ((_lastMousePosition - *_mouseDownPosition).manhattanLength() + >= QApplication::startDragDistance()) { + _dragging = true; + _startDraggingLeft = _scrollLeft; + } + } + checkDragging(); +} + +void TopPeersStrip::checkDragging() { + if (_dragging) { + const auto sign = (style::RightToLeft() ? -1 : 1); + const auto newLeft = std::clamp( + (sign * (_mouseDownPosition->x() - _lastMousePosition.x()) + + _startDraggingLeft), + 0, + _scrollLeftMax); + if (newLeft != _scrollLeft) { + _scrollLeft = newLeft; + update(); + } + } +} + +void TopPeersStrip::mouseReleaseEvent(QMouseEvent *e) { + _lastMousePosition = e->globalPos(); + const auto guard = gsl::finally([&] { + _mouseDownPosition = std::nullopt; + }); + + const auto pressed = std::exchange(_pressed, -1); + if (finishDragging()) { + return; + } + updateSelected(); + if (_selected == pressed) { + if (_selected < _entries.size()) { + _clicks.fire_copy(_entries[_selected].id); + } + } +} + +void TopPeersStrip::updateScrollMax() { + const auto &st = st::topPeers; + const auto single = st.photoLeft * 2 + st.photo; + const auto widthFull = int(_entries.size()) * single; + _scrollLeftMax = std::max(widthFull - width(), 0); + _scrollLeft = std::clamp(_scrollLeft, 0, _scrollLeftMax); + update(); +} + +bool TopPeersStrip::empty() const { + return _empty.current(); +} + +rpl::producer TopPeersStrip::emptyValue() const { + return _empty.value(); +} + +rpl::producer TopPeersStrip::clicks() const { + return _clicks.events(); +} + +auto TopPeersStrip::showMenuRequests() const +-> rpl::producer { + return _showMenuRequests.events(); +} + +void TopPeersStrip::apply(const TopPeersList &list) { + auto now = std::vector(); + + if (list.entries.empty()) { + _empty = true; + } + for (const auto &entry : list.entries) { + const auto i = ranges::find(_entries, entry.id, &Entry::id); + if (i != end(_entries)) { + now.push_back(base::take(*i)); + } else { + now.push_back({ .id = entry.id }); + } + apply(now.back(), entry); + } + for (auto &entry : _entries) { + if (entry.subscribed) { + entry.userpic->subscribeToUpdates(nullptr); + entry.subscribed = 0; + } + } + _entries = std::move(now); + if (!_entries.empty()) { + _empty = false; + } + update(); +} + +void TopPeersStrip::apply(Entry &entry, const TopPeersEntry &data) { + Expects(entry.id == data.id); + Expects(data.userpic != nullptr); + + if (entry.name.toString() != data.name) { + entry.name.setText(st::topPeers.nameStyle, data.name); + } + if (entry.userpic.get() != data.userpic.get()) { + if (entry.subscribed) { + entry.userpic->subscribeToUpdates(nullptr); + entry.subscribed = 0; + } + entry.userpic = data.userpic; + } +} + +void TopPeersStrip::paintEvent(QPaintEvent *e) { + auto p = Painter(this); + auto x = -_scrollLeft; + const auto &st = st::topPeers; + const auto line = st.lineTwice / 2; + const auto single = st.photoLeft * 2 + st.photo; + for (auto &entry : _entries) { + if (!entry.subscribed) { + entry.userpic->subscribeToUpdates([=] { + update(); + }); + entry.subscribed = 1; + } + const auto image = entry.userpic->image(st.photo); + p.drawImage( + QRect(x + st.photoLeft, st.photoTop, st.photo, st.photo), + image); + + const auto nameLeft = x + st.nameLeft; + entry.name.drawElided(p, nameLeft, st.nameTop, single, 1, style::al_top); + + x += single; + } +} + +void TopPeersStrip::contextMenuEvent(QContextMenuEvent *e) { + _menu = nullptr; + + if (e->reason() == QContextMenuEvent::Mouse) { + _lastMousePosition = e->globalPos(); + updateSelected(); + } + if (_selected < 0 || _entries.empty()) { + return; + } + _menu = base::make_unique_q( + this, + st::popupMenuWithIcons); + _showMenuRequests.fire({ + _entries[_selected].id, + Ui::Menu::CreateAddActionCallback(_menu), + }); + if (_menu->empty()) { + _menu = nullptr; + return; + } + const auto updateAfterMenuDestroyed = [=] { + const auto globalPosition = QCursor::pos(); + if (rect().contains(mapFromGlobal(globalPosition))) { + _lastMousePosition = globalPosition; + updateSelected(); + } + }; + QObject::connect( + _menu.get(), + &QObject::destroyed, + crl::guard(&_menuGuard, updateAfterMenuDestroyed)); + _menu->popup(e->globalPos()); + e->accept(); +} + +bool TopPeersStrip::finishDragging() { + if (!_dragging) { + return false; + } + checkDragging(); + _dragging = false; + updateSelected(); + return true; +} + +void TopPeersStrip::updateSelected() { + if (_pressed >= 0) { + return; + } + const auto &st = st::topPeers; + const auto p = mapFromGlobal(_lastMousePosition); + const auto x = p.x(); + const auto single = st.photoLeft * 2 + st.photo; + const auto index = (x - _scrollLeft) / single; + const auto selected = (index < 0 || index >= _entries.size()) + ? -1 + : index; + if (_selected != selected) { + const auto over = (selected >= 0); + if (over != (_selected >= 0)) { + setCursor(over ? style::cur_pointer : style::cur_default); + } + _selected = selected; + } +} + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h new file mode 100644 index 000000000..551b82480 --- /dev/null +++ b/Telegram/SourceFiles/dialogs/ui/top_peers_strip.h @@ -0,0 +1,93 @@ +/* +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/weak_ptr.h" +#include "ui/widgets/menu/menu_add_action_callback.h" +#include "ui/rp_widget.h" + +namespace Ui { +class DynamicImage; +} // namespace Ui + +namespace Dialogs { + +struct TopPeersEntry { + uint64 id = 0; + QString name; + std::shared_ptr userpic; + uint32 badge : 30 = 0; + uint32 muted : 1 = 0; + uint32 online : 1 = 0; +}; + +struct TopPeersList { + std::vector entries; +}; + +struct ShowTopPeerMenuRequest { + uint64 id = 0; + Ui::Menu::MenuCallback callback; +}; + +class TopPeersStrip final : public Ui::RpWidget { +public: + TopPeersStrip( + not_null parent, + rpl::producer content); + ~TopPeersStrip(); + + [[nodiscard]] bool empty() const; + [[nodiscard]] rpl::producer emptyValue() const; + [[nodiscard]] rpl::producer clicks() const; + [[nodiscard]] auto showMenuRequests() const + -> rpl::producer; + +private: + struct Entry; + + void resizeEvent(QResizeEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void wheelEvent(QWheelEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + + void updateScrollMax(); + void updateSelected(); + void checkDragging(); + bool finishDragging(); + + void apply(const TopPeersList &list); + void apply(Entry &entry, const TopPeersEntry &data); + + std::vector _entries; + rpl::variable _empty = true; + + rpl::event_stream _clicks; + rpl::event_stream _showMenuRequests; + rpl::event_stream> _verticalScrollEvents; + + QPoint _lastMousePosition; + std::optional _mouseDownPosition; + int _startDraggingLeft = 0; + int _scrollLeft = 0; + int _scrollLeftMax = 0; + bool _dragging = false; + Qt::Orientation _scrollingLock = {}; + + int _selected = -1; + int _pressed = -1; + + base::unique_qptr _menu; + base::has_weak_ptr _menuGuard; + +}; + +} // namespace Dialogs diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index c269dba39..94454a586 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/business/data_shortcut_messages.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" +#include "data/components/top_peers.h" #include "data/notify/data_notify_settings.h" #include "data/stickers/data_stickers.h" #include "data/data_drafts.h" @@ -429,9 +430,13 @@ not_null History::createItem( } return result; } - return message.match([&](const auto &data) { + const auto result = message.match([&](const auto &data) { return makeMessage(id, data, localFlags); }); + if (result->out() && result->isRegular()) { + session().topPeers().increment(peer, result->date()); + } + return result; } std::vector> History::createItems( diff --git a/Telegram/SourceFiles/main/main_session.cpp b/Telegram/SourceFiles/main/main_session.cpp index d0785b2bd..74b983062 100644 --- a/Telegram/SourceFiles/main/main_session.cpp +++ b/Telegram/SourceFiles/main/main_session.cpp @@ -28,8 +28,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/file_upload.h" #include "storage/storage_account.h" #include "storage/storage_facade.h" +#include "data/components/recent_peers.h" #include "data/components/scheduled_messages.h" #include "data/components/sponsored_messages.h" +#include "data/components/top_peers.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_user.h" @@ -100,6 +102,7 @@ Session::Session( , _attachWebView(std::make_unique(this)) , _scheduledMessages(std::make_unique(this)) , _sponsoredMessages(std::make_unique(this)) +, _topPeers(std::make_unique(this)) , _supportHelper(Support::Helper::Create(this)) , _saveSettingsTimer([=] { saveSettings(); }) { Expects(_settings != nullptr); diff --git a/Telegram/SourceFiles/main/main_session.h b/Telegram/SourceFiles/main/main_session.h index 4249f888f..717df1751 100644 --- a/Telegram/SourceFiles/main/main_session.h +++ b/Telegram/SourceFiles/main/main_session.h @@ -31,8 +31,10 @@ class Templates; namespace Data { class Session; class Changes; +class RecentPeers; class ScheduledMessages; class SponsoredMessages; +class TopPeers; } // namespace Data namespace Storage { @@ -106,12 +108,18 @@ public: [[nodiscard]] Data::Changes &changes() const { return *_changes; } + [[nodiscard]] Data::RecentPeers &recentPeers() const { + return *_recentPeers; + } [[nodiscard]] Data::SponsoredMessages &sponsoredMessages() const { return *_sponsoredMessages; } [[nodiscard]] Data::ScheduledMessages &scheduledMessages() const { return *_scheduledMessages; } + [[nodiscard]] Data::TopPeers &topPeers() const { + return *_topPeers; + } [[nodiscard]] Api::Updates &updates() const { return *_updates; } @@ -232,8 +240,10 @@ private: const std::unique_ptr _giftBoxStickersPacks; const std::unique_ptr _sendAsPeers; const std::unique_ptr _attachWebView; + const std::unique_ptr _recentPeers; const std::unique_ptr _scheduledMessages; const std::unique_ptr _sponsoredMessages; + const std::unique_ptr _topPeers; const std::unique_ptr _supportHelper; diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.cpp b/Telegram/SourceFiles/mtproto/mtproto_config.cpp index 4986f7190..454339f49 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_config.cpp +++ b/Telegram/SourceFiles/mtproto/mtproto_config.cpp @@ -45,7 +45,8 @@ QByteArray Config::serialize() const { + Serialize::stringSize(_fields.txtDomainString) + 3 * sizeof(qint32) + Serialize::stringSize(_fields.reactionDefaultEmoji) - + sizeof(quint64); + + sizeof(quint64) + + sizeof(qint32); auto result = QByteArray(); result.reserve(size); @@ -89,7 +90,8 @@ QByteArray Config::serialize() const { << qint32(_fields.blockedMode ? 1 : 0) << qint32(_fields.captionLengthMax) << _fields.reactionDefaultEmoji - << quint64(_fields.reactionDefaultCustom); + << quint64(_fields.reactionDefaultCustom) + << qint32(_fields.ratingDecay); } return result; } @@ -185,6 +187,9 @@ std::unique_ptr Config::FromSerialized(const QByteArray &serialized) { read(raw->_fields.reactionDefaultEmoji); read(raw->_fields.reactionDefaultCustom); } + if (!stream.atEnd()) { + read(raw->_fields.ratingDecay); + } if (stream.status() != QDataStream::Ok || !raw->_dcOptions.constructFromSerialized(dcOptionsSerialized)) { @@ -249,6 +254,10 @@ void Config::apply(const MTPDconfig &data) { }); } _fields.autologinToken = qs(data.vautologin_token().value_or_empty()); + _fields.ratingDecay = data.vrating_e_decay().v; + if (_fields.ratingDecay <= 0) { + _fields.ratingDecay = ConfigFields().ratingDecay; + } if (data.vdc_options().v.empty()) { LOG(("MTP Error: config with empty dc_options received!")); diff --git a/Telegram/SourceFiles/mtproto/mtproto_config.h b/Telegram/SourceFiles/mtproto/mtproto_config.h index fc2ed1c17..8a4db80df 100644 --- a/Telegram/SourceFiles/mtproto/mtproto_config.h +++ b/Telegram/SourceFiles/mtproto/mtproto_config.h @@ -39,6 +39,7 @@ struct ConfigFields { QString txtDomainString; bool blockedMode = false; int captionLengthMax = 1024; + int ratingDecay = 2419200; QString reactionDefaultEmoji = ConfigDefaultReactionEmoji(); uint64 reactionDefaultCustom; QString autologinToken; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 65bbc1eaa..5c0d6e173 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -87,6 +87,8 @@ PRIVATE dialogs/dialogs_three_state_icon.h dialogs/ui/dialogs_stories_list.cpp dialogs/ui/dialogs_stories_list.h + dialogs/ui/top_peers_strip.cpp + dialogs/ui/top_peers_strip.h editor/controllers/undo_controller.cpp editor/controllers/undo_controller.h