From 168711b352c31357cd93118fa78aa4bb05433081 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 30 Nov 2021 20:38:47 +0400 Subject: [PATCH] Use PeerList for sessions list (wip). --- Telegram/SourceFiles/boxes/sessions_box.cpp | 499 +++++++++++-------- Telegram/SourceFiles/settings/settings.style | 47 +- 2 files changed, 316 insertions(+), 230 deletions(-) diff --git a/Telegram/SourceFiles/boxes/sessions_box.cpp b/Telegram/SourceFiles/boxes/sessions_box.cpp index 501fdd4289..630fcb304e 100644 --- a/Telegram/SourceFiles/boxes/sessions_box.cpp +++ b/Telegram/SourceFiles/boxes/sessions_box.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/algorithm.h" #include "base/platform/base_platform_info.h" #include "boxes/self_destruction_box.h" +#include "boxes/peer_lists_box.h" #include "ui/boxes/confirm_box.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -57,6 +58,50 @@ enum class Type { Other, }; +class Row; + +class RowDelegate { +public: + virtual void rowUpdateRow(not_null row) = 0; +}; + +class Row final : public PeerListRow { +public: + Row(not_null delegate, const EntryData &data); + + void update(const EntryData &data); + void updateName(const QString &name); + + [[nodiscard]] EntryData data() const; + + QString generateName() override; + QString generateShortName() override; + PaintRoundImageCallback generatePaintUserpicCallback() override; + + int elementsCount() const override; + QRect elementGeometry(int element, int outerWidth) const override; + bool elementDisabled(int element) const override; + bool elementOnlySelect(int element) const override; + void elementAddRipple( + int element, + QPoint point, + Fn updateCallback) override; + void elementsStopLastRipple() override; + void elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) override; + +private: + const not_null _delegate; + Ui::Text::String _location; + Type _type = Type::Other; + EntryData _data; + QImage _userpic; + +}; + void RenameBox(not_null box) { box->setTitle(tr::lng_settings_rename_device_title()); @@ -255,7 +300,7 @@ void RenameBox(not_null box) { } [[nodiscard]] QImage GenerateUserpic(Type type) { - const auto size = st::sessionUserpicSize; + const auto size = st::sessionListItem.photoSize; const auto full = size * style::DevicePixelRatio(); const auto rect = QRect(0, 0, size, size); @@ -440,6 +485,125 @@ void SessionInfoBox( } } +Row::Row(not_null delegate, const EntryData &data) +: PeerListRow(data.hash) +, _delegate(delegate) +, _location(st::defaultTextStyle, LocationAndDate(data)) +, _type(TypeFromEntry(data)) +, _data(data) +, _userpic(GenerateUserpic(_type)) { + setCustomStatus(_data.info); +} + +void Row::update(const EntryData &data) { + _data = data; + setCustomStatus(_data.info); + refreshName(st::sessionListItem); + _location.setText(st::defaultTextStyle, LocationAndDate(_data)); + _delegate->rowUpdateRow(this); +} + +void Row::updateName(const QString &name) { + _data.name = name; + refreshName(st::sessionListItem); + _delegate->rowUpdateRow(this); +} + +EntryData Row::data() const { + return _data; +} + +QString Row::generateName() { + return _data.name; +} + +QString Row::generateShortName() { + return generateName(); +} + +PaintRoundImageCallback Row::generatePaintUserpicCallback() { + return [=]( + Painter &p, + int x, + int y, + int outerWidth, + int size) { + p.drawImage(x, y, _userpic); + }; +} + +int Row::elementsCount() const { + return 2; +} + +QRect Row::elementGeometry(int element, int outerWidth) const { + switch (element) { + case 1: { + return QRect( + st::sessionListItem.namePosition.x(), + st::sessionLocationTop, + outerWidth, + st::normalFont->height); + } break; + case 2: { + const auto size = QSize( + st::sessionListThreeDotsIcon.width(), + st::sessionListThreeDotsIcon.height()); + const auto margins = QMargins( + 0, + (st::sessionListItem.height - size.height()) / 2, + st::sessionListThreeDotsSkip, + 0); + const auto right = margins.right(); + const auto top = margins.top(); + const auto left = outerWidth - right - size.width(); + return QRect(QPoint(left, top), size); + } break; + } + return QRect(); +} + +bool Row::elementDisabled(int element) const { + return !id() || (element == 1); +} + +bool Row::elementOnlySelect(int element) const { + return false; +} + +void Row::elementAddRipple( + int element, + QPoint point, + Fn updateCallback) { +} + +void Row::elementsStopLastRipple() { +} + +void Row::elementsPaint( + Painter &p, + int outerWidth, + bool selected, + int selectedElement) { + if (id()) { + const auto geometry = elementGeometry(2, outerWidth); + const auto &icon = (selectedElement == 2) + ? st::sessionListThreeDotsIconOver + : st::sessionListThreeDotsIcon; + icon.paint(p, geometry.x(), geometry.y(), outerWidth); + } + p.setFont(st::msgFont); + p.setPen(st::sessionInfoFg); + const auto locationLeft = st::sessionListItem.namePosition.x(); + const auto available = outerWidth - locationLeft; + _location.drawLeftElided( + p, + locationLeft, + st::sessionLocationTop, + available, + outerWidth); +} + } // namespace class SessionsContent : public Ui::RpWidget { @@ -455,25 +619,13 @@ protected: void paintEvent(QPaintEvent *e) override; private: - struct Entry { - Entry() = default; - explicit Entry(const EntryData &entry); - - EntryData data; - - bool incomplete = false; - Type type = Type::Other; - TimeId activeTime = 0; - Ui::Text::String name, info, location; - QImage userpic; - }; struct Full { - Entry current; - std::vector incomplete; - std::vector list; + EntryData current; + std::vector incomplete; + std::vector list; }; class Inner; - class List; + class ListController; void shortPollSessions(); void parse(const Api::Authorizations::List &list); @@ -495,44 +647,39 @@ private: }; -class SessionsContent::List : public Ui::RpWidget { +class SessionsContent::ListController final + : public PeerListController + , public RowDelegate + , public base::has_weak_ptr { public: - List(QWidget *parent); + explicit ListController(not_null session); - void showData(gsl::span items); + Main::Session &session() const override; + void prepare() override; + void rowClicked(not_null row) override; + void rowElementClicked(not_null row, int element) override; + + void rowUpdateRow(not_null row) override; + + void showData(gsl::span items); rpl::producer itemsCount() const; rpl::producer terminateRequests() const; [[nodiscard]] rpl::producer showRequests() const; - void terminating(uint64 hash, bool terminating); - -protected: - void resizeEvent(QResizeEvent *e) override; - void paintEvent(QPaintEvent *e) override; - - void mousePressEvent(QMouseEvent *e) override; - void mouseReleaseEvent(QMouseEvent *e) override; - - int resizeGetHeight(int newWidth) override; + [[nodiscard]] static std::unique_ptr Add( + not_null container, + not_null session, + style::margins margins = {}); private: - struct RowWidth { - int available = 0; - int info = 0; - }; - void subscribeToCustomDeviceModel(); - void computeRowWidth(); - RowWidth _rowWidth; - std::vector _items; - std::map> _terminateButtons; + const not_null _session; + rpl::event_stream _terminateRequests; rpl::event_stream _itemsCount; rpl::event_stream _showRequests; - int _pressed = -1; - }; class SessionsContent::Inner : public Ui::RpWidget { @@ -546,32 +693,20 @@ public: [[nodiscard]] rpl::producer showRequests() const; [[nodiscard]] rpl::producer terminateOne() const; [[nodiscard]] rpl::producer<> terminateAll() const; - [[nodiscard]] rpl::producer<> renameCurrentRequests() const; - - void terminatingOne(uint64 hash, bool terminating); private: void setupContent(); const not_null _controller; - QPointer _current; + std::unique_ptr _current; QPointer _terminateAll; - QPointer _incomplete; - QPointer _list; + std::unique_ptr _incomplete; + std::unique_ptr _list; rpl::variable _ttlDays; }; -SessionsContent::Entry::Entry(const EntryData &entry) -: data(entry) -, incomplete(entry.incomplete) -, type(TypeFromEntry(entry)) -, activeTime(entry.activeTime) -, name(st::sessionNameStyle, entry.name) -, info(st::sessionInfoStyle, entry.info) -, location(st::sessionInfoStyle, LocationAndDate(entry)) -, userpic(GenerateUserpic(type)) { -}; +//, location(st::sessionInfoStyle, LocationAndDate(entry)) SessionsContent::SessionsContent( QWidget*, @@ -629,20 +764,19 @@ void SessionsContent::parse(const Api::Authorizations::List &list) { } _data = Full(); for (const auto &auth : list) { - auto entry = Entry(auth); - if (!entry.data.hash) { - _data.current = std::move(entry); - } else if (entry.incomplete) { - _data.incomplete.push_back(std::move(entry)); + if (!auth.hash) { + _data.current = auth; + } else if (auth.incomplete) { + _data.incomplete.push_back(auth); } else { - _data.list.push_back(std::move(entry)); + _data.list.push_back(auth); } } _loading = false; - ranges::sort(_data.list, std::greater<>(), &Entry::activeTime); - ranges::sort(_data.incomplete, std::greater<>(), &Entry::activeTime); + ranges::sort(_data.list, std::greater<>(), &EntryData::activeTime); + ranges::sort(_data.incomplete, std::greater<>(), &EntryData::activeTime); _inner->showData(_data); @@ -710,13 +844,12 @@ void SessionsContent::terminateOne(uint64 hash) { if (mtpIsFalse(result)) { return; } - _inner->terminatingOne(hash, false); - const auto removeByHash = [&](std::vector &list) { + const auto removeByHash = [&](std::vector &list) { list.erase( ranges::remove( list, hash, - [](const Entry &entry) { return entry.data.hash; }), + [](const EntryData &entry) { return entry.hash; }), end(list)); }; removeByHash(_data.incomplete); @@ -724,13 +857,11 @@ void SessionsContent::terminateOne(uint64 hash) { _inner->showData(_data); }); auto fail = crl::guard(weak, [=](const MTP::Error &error) { - _inner->terminatingOne(hash, false); }); _authorizations->requestTerminate( std::move(done), std::move(fail), hash); - _inner->terminatingOne(hash, true); }; terminate(std::move(callback), tr::lng_settings_reset_one_sure(tr::now)); } @@ -788,8 +919,10 @@ void SessionsContent::Inner::setupContent() { Ui::show(Box(RenameBox), Ui::LayerOption::KeepOther); }); - _current = content->add( - object_ptr(content), + const auto session = &_controller->session(); + _current = ListController::Add( + content, + session, style::margins{ 0, 0, 0, st::sessionCurrentSkip }); const auto terminateWrap = content->add( object_ptr>( @@ -814,7 +947,7 @@ void SessionsContent::Inner::setupContent() { const auto incompleteInner = incompleteWrap->entity(); AddSkip(incompleteInner, st::sessionSubtitleSkip); AddSubsectionTitle(incompleteInner, tr::lng_sessions_incomplete()); - _incomplete = incompleteInner->add(object_ptr(incompleteInner)); + _incomplete = ListController::Add(incompleteInner, session); AddSkip(incompleteInner); AddDividerText(incompleteInner, tr::lng_sessions_incomplete_about()); @@ -825,7 +958,7 @@ void SessionsContent::Inner::setupContent() { const auto listInner = listWrap->entity(); AddSkip(listInner, st::sessionSubtitleSkip); AddSubsectionTitle(listInner, tr::lng_sessions_other_header()); - _list = listInner->add(object_ptr(listInner)); + _list = ListController::Add(listInner, session); AddSkip(listInner); AddDividerText(listInner, tr::lng_sessions_about_apps()); @@ -897,165 +1030,113 @@ rpl::producer SessionsContent::Inner::showRequests() const { _list->showRequests()); } -void SessionsContent::Inner::terminatingOne(uint64 hash, bool terminating) { - _incomplete->terminating(hash, terminating); - _list->terminating(hash, terminating); +SessionsContent::ListController::ListController( + not_null session) +: _session(session) { } -SessionsContent::List::List(QWidget *parent) : RpWidget(parent) { - setAttribute(Qt::WA_OpaquePaintEvent); +Main::Session &SessionsContent::ListController::session() const { + return *_session; } -void SessionsContent::List::resizeEvent(QResizeEvent *e) { - RpWidget::resizeEvent(e); - - computeRowWidth(); -} - -void SessionsContent::List::subscribeToCustomDeviceModel() { +void SessionsContent::ListController::subscribeToCustomDeviceModel() { Core::App().settings().deviceModelChanges( ) | rpl::start_with_next([=](const QString &model) { - for (auto &entry : _items) { - if (!entry.data.hash) { - entry.name.setText(st::sessionNameStyle, model); + for (auto i = 0; i != delegate()->peerListFullRowsCount(); ++i) { + const auto row = delegate()->peerListRowAt(i); + if (!row->id()) { + static_cast(row.get())->updateName(model); } } - update(); }, lifetime()); } -void SessionsContent::List::showData(gsl::span items) { - computeRowWidth(); +void SessionsContent::ListController::prepare() { +} - auto buttons = base::take(_terminateButtons); - _items.clear(); - _items.insert(begin(_items), items.begin(), items.end()); - for (const auto &entry : _items) { - const auto hash = entry.data.hash; - if (!hash) { +void SessionsContent::ListController::rowClicked( + not_null row) { + _showRequests.fire_copy(static_cast(row.get())->data()); +} + +void SessionsContent::ListController::rowElementClicked( + not_null row, + int element) { + if (element == 1) { + if (const auto hash = static_cast(row.get())->data().hash) { + _terminateRequests.fire_copy(hash); + } + } +} + +void SessionsContent::ListController::rowUpdateRow(not_null row) { + delegate()->peerListUpdateRow(row); +} + +void SessionsContent::ListController::showData( + gsl::span items) { + auto index = 0; + auto positions = base::flat_map(); + positions.reserve(items.size()); + for (const auto &entry : items) { + const auto id = entry.hash; + positions.emplace(id, index++); + if (const auto row = delegate()->peerListFindRow(id)) { + static_cast(row)->update(entry); + } else { + delegate()->peerListAppendRow( + std::make_unique(this, entry)); + } + } + for (auto i = 0; i != delegate()->peerListFullRowsCount();) { + const auto row = delegate()->peerListRowAt(i); + if (positions.contains(row->id())) { + ++i; continue; } - const auto button = [&] { - const auto i = buttons.find(hash); - return _terminateButtons.emplace( - hash, - (i != end(buttons) - ? std::move(i->second) - : std::make_unique( - this, - st::sessionTerminate))).first->second.get(); - }(); - button->setClickedCallback([=] { - _terminateRequests.fire_copy(hash); - }); - button->show(); - const auto number = _terminateButtons.size() - 1; - widthValue( - ) | rpl::start_with_next([=] { - button->moveToRight( - st::sessionTerminateSkip, - (number * st::sessionHeight + st::sessionTerminateTop)); - }, lifetime()); + delegate()->peerListRemoveRow(row); } - resizeToWidth(width()); - _itemsCount.fire(_items.size()); + delegate()->peerListSortRows([&]( + const PeerListRow &a, + const PeerListRow &b) { + return positions[a.id()] < positions[b.id()]; + }); + delegate()->peerListRefreshRows(); + _itemsCount.fire(delegate()->peerListFullRowsCount()); } -rpl::producer SessionsContent::List::showRequests() const { - return _showRequests.events(); +rpl::producer SessionsContent::ListController::itemsCount() const { + return _itemsCount.events_starting_with( + delegate()->peerListFullRowsCount()); } -rpl::producer SessionsContent::List::itemsCount() const { - return _itemsCount.events_starting_with(_items.size()); -} - -rpl::producer SessionsContent::List::terminateRequests() const { +rpl::producer SessionsContent::ListController::terminateRequests() const { return _terminateRequests.events(); } -void SessionsContent::List::terminating(uint64 hash, bool terminating) { - const auto i = _terminateButtons.find(hash); - if (i != _terminateButtons.cend()) { - if (terminating) { - i->second->clearState(); - i->second->hide(); - } else { - i->second->show(); - } - } +rpl::producer SessionsContent::ListController::showRequests() const { + return _showRequests.events(); } -int SessionsContent::List::resizeGetHeight(int newWidth) { - return _items.size() * st::sessionHeight; -} - -void SessionsContent::List::computeRowWidth() { - const auto available = width() - - st::sessionPadding.left() - - st::sessionTerminateSkip; - _rowWidth = { - .available = available, - .info = available - st::sessionTerminate.width, - }; -} - -void SessionsContent::List::paintEvent(QPaintEvent *e) { - QRect r(e->rect()); - Painter p(this); - - p.fillRect(r, st::boxBg); - p.setFont(st::linkFont); - const auto count = int(_items.size()); - const auto from = floorclamp(r.y(), st::sessionHeight, 0, count); - const auto till = ceilclamp( - r.y() + r.height(), - st::sessionHeight, - 0, - count); - - const auto available = _rowWidth.available; - const auto x = st::sessionPadding.left(); - const auto y = st::sessionPadding.top(); - const auto w = width(); - p.translate(0, from * st::sessionHeight); - for (auto i = from; i != till; ++i) { - const auto &entry = _items[i]; - - p.drawImage(st::sessionUserpicPosition, entry.userpic); - - const auto nameW = _rowWidth.info; - const auto infoW = entry.data.hash ? _rowWidth.info : available; - - p.setPen(st::sessionNameFg); - entry.name.drawLeftElided(p, x, y, nameW, w); - - p.setPen(st::boxTextFg); - entry.info.drawLeftElided(p, x, y + st::sessionInfoTop, infoW, w); - - p.setPen(st::sessionInfoFg); - entry.location.drawLeftElided( - p, - x, - y + st::sessionLocationTop, - available, - w); - - p.translate(0, st::sessionHeight); - } -} - -void SessionsContent::List::mousePressEvent(QMouseEvent *e) { - const auto index = e->pos().y() / st::sessionHeight; - _pressed = (index >= 0 && index < _items.size()) ? index : -1; -} - -void SessionsContent::List::mouseReleaseEvent(QMouseEvent *e) { - const auto index = e->pos().y() / st::sessionHeight; - const auto released = (index >= 0 && index < _items.size()) ? index : -1; - if (released == _pressed && released >= 0) { - _showRequests.fire_copy(_items[released].data); - } - _pressed = -1; +auto SessionsContent::ListController::Add( + not_null container, + not_null session, + style::margins margins) +-> std::unique_ptr { + auto &lifetime = container->lifetime(); + const auto delegate = lifetime.make_state< + PeerListContentDelegateSimple + >(); + auto controller = std::make_unique(session); + controller->setStyleOverrides(&st::sessionList); + const auto content = container->add( + object_ptr( + container, + controller.get()), + margins); + delegate->setContent(content); + controller->setDelegate(delegate); + return controller; } SessionsBox::SessionsBox( diff --git a/Telegram/SourceFiles/settings/settings.style b/Telegram/SourceFiles/settings/settings.style index d56433435e..c3a5d7122e 100644 --- a/Telegram/SourceFiles/settings/settings.style +++ b/Telegram/SourceFiles/settings/settings.style @@ -249,23 +249,13 @@ sessionsTerminateAll: SettingsButton(defaultSettingsButton) { } sessionsTerminateAllIcon: icon {{ "settings/devices/terminate_all", attentionButtonFg }}; sessionsTerminateAllIconLeft: 30px; -sessionHeight: 84px; -sessionInfoTop: 21px; -sessionLocationTop: 43px; +sessionLocationTop: 54px; sessionCurrentSkip: 8px; sessionSubtitleSkip: 14px; -sessionPadding: margins(77px, 11px, 22px, 0px); -sessionNameFont: msgNameFont; -sessionNameFg: boxTextFg; -sessionWhenFont: msgDateFont; -sessionWhenFg: windowSubTextFg; -sessionInfoFont: msgFont; +sessionLocationFg: windowSubTextFg; sessionInfoFg: windowSubTextFg; sessionTerminateTop: 9px; sessionTerminateSkip: 22px; -sessionUserpicSize: 42px; -sessionUserpicPosition: point(21px, 10px); -sessionNamePadding: margins(0px, 0px, 5px, 0px); sessionTerminate: IconButton { width: 20px; height: 20px; @@ -280,15 +270,6 @@ sessionTerminate: IconButton { color: windowBgOver; } } -sessionNameStyle: TextStyle(defaultTextStyle) { - font: sessionNameFont; -} -sessionWhenStyle: TextStyle(defaultTextStyle) { - font: sessionWhenFont; -} -sessionInfoStyle: TextStyle(defaultTextStyle) { - font: sessionInfoFont; -} sessionIconWindows: icon{{ "settings/devices/device_desktop_win", historyPeerUserpicFg }}; sessionIconMac: icon{{ "settings/devices/device_desktop_mac", historyPeerUserpicFg }}; sessionIconUbuntu: icon{{ "settings/devices/device_linux_ubuntu", historyPeerUserpicFg }}; @@ -327,3 +308,27 @@ sessionValueLabel: FlatLabel(defaultFlatLabel) { textFg: windowSubTextFg; } sessionValueSkip: 8px; + +sessionListItem: PeerListItem(defaultPeerListItem) { + button: OutlineButton(defaultPeerListButton) { + font: normalFont; + padding: margins(11px, 5px, 11px, 5px); + } + height: 84px; + photoPosition: point(21px, 10px); + nameStyle: TextStyle(defaultTextStyle) { + font: msgNameFont; + } + namePosition: point(77px, 11px); + statusPosition: point(77px, 32px); + photoSize: 42px; + statusFg: boxTextFg; + statusFgOver: boxTextFg; +} +sessionList: PeerList(defaultPeerList) { + item: sessionListItem; + padding: margins(0px, 4px, 0px, 0px); +} +sessionListThreeDotsIcon: icon {{ "info/edit/dotsmini", dialogsMenuIconFg }}; +sessionListThreeDotsIconOver: icon {{ "info/edit/dotsmini", dialogsMenuIconFgOver }}; +sessionListThreeDotsSkip: 12px;