Initial version of top peers.

This commit is contained in:
John Preston 2024-04-09 19:04:18 +04:00
parent 11e4c45969
commit 18598f8dca
21 changed files with 908 additions and 27 deletions

View file

@ -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

View file

@ -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);
}

View file

@ -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<Main::Session*> session)
: _session(session) {
}
RecentPeers::~RecentPeers() = default;
} // namespace Data

View file

@ -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<Main::Session*> session);
~RecentPeers();
private:
const not_null<Main::Session*> _session;
};
} // namespace Data

View file

@ -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<Main::Session*> 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<not_null<PeerData*>> 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<PeerData*> 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

View file

@ -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<Main::Session*> session);
~TopPeers();
[[nodiscard]] std::vector<not_null<PeerData*>> list() const;
[[nodiscard]] bool disabled() const;
[[nodiscard]] rpl::producer<> updates() const;
void increment(not_null<PeerData*> peer, TimeId date);
void reload();
private:
struct TopPeer {
not_null<PeerData*> peer;
float64 rating = 0.;
};
void request();
[[nodiscard]] uint64 countHash() const;
const not_null<Main::Session*> _session;
std::vector<TopPeer> _list;
rpl::event_stream<> _updates;
crl::time _lastReceived = 0;
TimeId _lastReceivedDate = 0;
mtpRequestId _requestId = 0;
bool _disabled = false;
bool _received = false;
};
} // namespace Data

View file

@ -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;

View file

@ -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<QWidget*> 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<Suggestions>(
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);

View file

@ -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<Ui::ElasticScroll> _scroll;
QPointer<InnerWidget> _inner;
std::unique_ptr<Suggestions> _suggestions;
class BottomButton;
object_ptr<BottomButton> _updateTelegram = { nullptr };
object_ptr<BottomButton> _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<Data::ReactionId> _searchTags;

View file

@ -184,9 +184,9 @@ rpl::producer<bool> 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();

View file

@ -94,7 +94,7 @@ public:
[[nodiscard]] rpl::producer<uint64> clicks() const;
[[nodiscard]] rpl::producer<ShowMenuRequest> showMenuRequests() const;
[[nodiscard]] rpl::producer<bool> 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<uint64> _clicks;
rpl::event_stream<ShowMenuRequest> _showMenuRequests;
rpl::event_stream<bool> _toggleExpandedRequests;
rpl::event_stream<> _entered;
//rpl::event_stream<> _entered;
rpl::event_stream<> _loadMoreRequests;
rpl::event_stream<> _collapsedGeometryChanged;

View file

@ -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<QWidget*> parent,
rpl::producer<TopPeersList> topPeers)
: RpWidget(parent)
, _scroll(std::make_unique<Ui::ElasticScroll>(this))
, _content(_scroll->setOwnedWidget(object_ptr<Ui::VerticalLayout>(this)))
, _topPeersWrap(_content->add(object_ptr<Ui::SlideWrap<TopPeersStrip>>(
this,
object_ptr<TopPeersStrip>(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<Ui::RpWidget> Suggestions::setupDivider() {
auto result = object_ptr<Ui::DividerLabel>(
this,
object_ptr<Ui::FlatLabel>(
this,
rpl::single(u"Recent"_q),
st::boxDividerLabel),
st::defaultBoxDividerLabelPadding);
return result;
}
TopPeersList TopPeersContent(not_null<Main::Session*> 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

View file

@ -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 <typename Widget>
class SlideWrap;
} // namespace Ui
namespace Dialogs {
class Suggestions final : public Ui::RpWidget {
public:
Suggestions(
not_null<QWidget*> parent,
rpl::producer<TopPeersList> topPeers);
~Suggestions();
[[nodiscard]] rpl::producer<PeerId> topPeerChosen() const {
return _topPeerChosen.events();
}
private:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
[[nodiscard]] object_ptr<Ui::RpWidget> setupDivider();
void updateControlsGeometry();
const std::unique_ptr<Ui::ElasticScroll> _scroll;
const not_null<Ui::VerticalLayout*> _content;
const not_null<Ui::SlideWrap<TopPeersStrip>*> _topPeersWrap;
const not_null<TopPeersStrip*> _topPeers;
const not_null<Ui::RpWidget*> _divider;
rpl::event_stream<PeerId> _topPeerChosen;
};
[[nodiscard]] TopPeersList TopPeersContent(
not_null<Main::Session*> session);
} // namespace Dialogs

View file

@ -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 <QtWidgets/QApplication>
namespace Dialogs {
struct TopPeersStrip::Entry {
uint64 id = 0;
Ui::Text::String name;
std::shared_ptr<Ui::DynamicImage> userpic;
bool subscribed = false;
};
TopPeersStrip::TopPeersStrip(
not_null<QWidget*> parent,
rpl::producer<TopPeersList> 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<bool> TopPeersStrip::emptyValue() const {
return _empty.value();
}
rpl::producer<uint64> TopPeersStrip::clicks() const {
return _clicks.events();
}
auto TopPeersStrip::showMenuRequests() const
-> rpl::producer<ShowTopPeerMenuRequest> {
return _showMenuRequests.events();
}
void TopPeersStrip::apply(const TopPeersList &list) {
auto now = std::vector<Entry>();
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<Ui::PopupMenu>(
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

View file

@ -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<Ui::DynamicImage> userpic;
uint32 badge : 30 = 0;
uint32 muted : 1 = 0;
uint32 online : 1 = 0;
};
struct TopPeersList {
std::vector<TopPeersEntry> entries;
};
struct ShowTopPeerMenuRequest {
uint64 id = 0;
Ui::Menu::MenuCallback callback;
};
class TopPeersStrip final : public Ui::RpWidget {
public:
TopPeersStrip(
not_null<QWidget*> parent,
rpl::producer<TopPeersList> content);
~TopPeersStrip();
[[nodiscard]] bool empty() const;
[[nodiscard]] rpl::producer<bool> emptyValue() const;
[[nodiscard]] rpl::producer<uint64> clicks() const;
[[nodiscard]] auto showMenuRequests() const
-> rpl::producer<ShowTopPeerMenuRequest>;
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<Entry> _entries;
rpl::variable<bool> _empty = true;
rpl::event_stream<uint64> _clicks;
rpl::event_stream<ShowTopPeerMenuRequest> _showMenuRequests;
rpl::event_stream<not_null<QWheelEvent*>> _verticalScrollEvents;
QPoint _lastMousePosition;
std::optional<QPoint> _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<Ui::PopupMenu> _menu;
base::has_weak_ptr _menuGuard;
};
} // namespace Dialogs

View file

@ -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<HistoryItem*> 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<not_null<HistoryItem*>> History::createItems(

View file

@ -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<InlineBots::AttachWebView>(this))
, _scheduledMessages(std::make_unique<Data::ScheduledMessages>(this))
, _sponsoredMessages(std::make_unique<Data::SponsoredMessages>(this))
, _topPeers(std::make_unique<Data::TopPeers>(this))
, _supportHelper(Support::Helper::Create(this))
, _saveSettingsTimer([=] { saveSettings(); }) {
Expects(_settings != nullptr);

View file

@ -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<Stickers::GiftBoxPack> _giftBoxStickersPacks;
const std::unique_ptr<SendAsPeers> _sendAsPeers;
const std::unique_ptr<InlineBots::AttachWebView> _attachWebView;
const std::unique_ptr<Data::RecentPeers> _recentPeers;
const std::unique_ptr<Data::ScheduledMessages> _scheduledMessages;
const std::unique_ptr<Data::SponsoredMessages> _sponsoredMessages;
const std::unique_ptr<Data::TopPeers> _topPeers;
const std::unique_ptr<Support::Helper> _supportHelper;

View file

@ -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> 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!"));

View file

@ -39,6 +39,7 @@ struct ConfigFields {
QString txtDomainString;
bool blockedMode = false;
int captionLengthMax = 1024;
int ratingDecay = 2419200;
QString reactionDefaultEmoji = ConfigDefaultReactionEmoji();
uint64 reactionDefaultCustom;
QString autologinToken;

View file

@ -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