From 451c4e3101c0eefac461a679ce2aa728f4a88f35 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 4 Jul 2023 00:05:11 +0400 Subject: [PATCH] Implement vertical list of hidden story sources. --- Telegram/Resources/langs/lang.strings | 3 + Telegram/SourceFiles/boxes/boxes.style | 10 + Telegram/SourceFiles/boxes/peer_list_box.cpp | 9 +- Telegram/SourceFiles/boxes/peer_list_box.h | 3 + .../boxes/peer_list_controllers.cpp | 404 +++++++++++++----- Telegram/SourceFiles/data/data_stories.cpp | 21 +- Telegram/SourceFiles/data/data_stories.h | 7 +- .../dialogs/ui/dialogs_stories_content.cpp | 8 +- .../dialogs/ui/dialogs_stories_list.cpp | 19 +- .../dialogs/ui/dialogs_stories_list.h | 5 +- .../stories/info_stories_inner_widget.cpp | 2 +- .../SourceFiles/ui/effects/round_checkbox.cpp | 56 ++- .../SourceFiles/ui/effects/round_checkbox.h | 10 +- 13 files changed, 417 insertions(+), 140 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index fa47c0a996..a816e9883c 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2407,6 +2407,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_add_contact" = "Create"; "lng_add_contact_button" = "New contact"; "lng_contacts_header" = "Contacts"; +"lng_contacts_by_online" = "Sorted by last seen time"; +"lng_contacts_by_name" = "Sorted by name"; +"lng_contacts_hidden_stories" = "Hidden Stories"; "lng_contact_not_joined" = "Unfortunately {name} has not joined Telegram yet, but you can send them an invitation.\n\nWe will notify you about any of your contacts who join Telegram."; "lng_try_other_contact" = "Try someone else"; "lng_create_group_link" = "Link"; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 4dba7e7238..4ba5c01b50 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -942,6 +942,16 @@ requestsBoxList: PeerList(peerListBox) { padding: margins(0px, 12px, 0px, 12px); item: requestsBoxItem; } +contactsWithStories: PeerList(peerListBox) { + item: PeerListItem(peerListBoxItem) { + checkbox: RoundImageCheckbox(defaultPeerListCheckbox) { + check: RoundCheckbox(defaultPeerListCheck) { + size: 0px; + } + } + nameFgChecked: contactsNameFg; + } +} requestsAcceptButton: RoundButton(defaultActiveButton) { width: -28px; height: 30px; diff --git a/Telegram/SourceFiles/boxes/peer_list_box.cpp b/Telegram/SourceFiles/boxes/peer_list_box.cpp index 2ceba422b8..95920addcf 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_box.cpp @@ -33,8 +33,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_dialogs.h" #include "styles/style_widgets.h" -#include - PaintRoundImageCallback PaintUserpicCallback( not_null peer, bool respectSavedMessagesChat) { @@ -887,6 +885,13 @@ void PeerListRow::setCheckedInternal(bool checked, anim::type animated) { _checkbox->setChecked(checked, animated); } +void PeerListRow::setCustomizedCheckSegments( + std::vector segments) { + Expects(_checkbox != nullptr); + + _checkbox->setCustomizedSegments(std::move(segments)); +} + void PeerListRow::finishCheckedAnimation() { _checkbox->setChecked(_checkbox->checked(), anim::type::instant); } diff --git a/Telegram/SourceFiles/boxes/peer_list_box.h b/Telegram/SourceFiles/boxes/peer_list_box.h index 28cb695341..cfa5b66bcd 100644 --- a/Telegram/SourceFiles/boxes/peer_list_box.h +++ b/Telegram/SourceFiles/boxes/peer_list_box.h @@ -36,6 +36,7 @@ class SlideWrap; class FlatLabel; struct ScrollToRequest; class PopupMenu; +struct RoundImageCheckboxSegment; } // namespace Ui using PaintRoundImageCallback = Fn segments); void setHidden(bool hidden) { _hidden = hidden; } diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 2bf72ee92c..6c0343b807 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -9,8 +9,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "api/api_chat_participants.h" #include "base/random.h" +#include "boxes/filters/edit_filter_chats_list.h" #include "ui/boxes/confirm_box.h" +#include "ui/effects/round_checkbox.h" +#include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/checkbox.h" +#include "ui/widgets/popup_menu.h" #include "ui/wrap/padding_wrap.h" #include "ui/painter.h" #include "ui/ui_utility.h" @@ -32,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "dialogs/dialogs_main_list.h" +#include "ui/wrap/slide_wrap.h" #include "window/window_session_controller.h" // showAddContact() #include "base/unixtime.h" #include "styles/style_boxes.h" @@ -42,12 +47,302 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "dialogs/ui/dialogs_stories_content.h" #include "dialogs/ui/dialogs_stories_list.h" - namespace { constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000); constexpr auto kSearchPerPage = 50; +class StoriesRow final : public PeerListRow { +public: + StoriesRow( + not_null session, + const QBrush &unread, + const Dialogs::Stories::Element &element, + int position); + + void applySegments(const Dialogs::Stories::Element &element); + void updateGradient(QBrush unread); + + int position = 0; + +private: + void refreshSegments(); + + QBrush _unread; + int _count = 0; + int _unreadCount = 0; + +}; + +class StoriesController final + : public PeerListController + , public base::has_weak_ptr { +public: + using Content = Dialogs::Stories::Content; + using Row = StoriesRow; + + StoriesController( + not_null window, + rpl::producer content, + Fn open, + Fn loadMore); + + Main::Session &session() const override; + void prepare() override; + void loadMoreRows() override; + void rowClicked(not_null row) override; + base::unique_qptr rowContextMenu( + QWidget *parent, + not_null row) override; + +private: + void refresh(const Content &content); + + const not_null _window; + QBrush _unread; + rpl::producer _content; + Fn _open; + Fn _loadMore; + bool _positionPositive = false; + + rpl::lifetime _lifetime; + +}; + +StoriesRow::StoriesRow( + not_null session, + const QBrush &unread, + const Dialogs::Stories::Element &element, + int position) +: PeerListRow(session->data().peer(PeerId(element.id))) +, position(position) +, _unread(unread) { +} + +void StoriesRow::applySegments(const Dialogs::Stories::Element &element) { + Expects(element.unreadCount <= element.count); + + _count = std::max(element.count, 1); + _unreadCount = element.unreadCount; + refreshSegments(); +} + +void StoriesRow::updateGradient(QBrush unread) { + _unread = std::move(unread); + refreshSegments(); +} + +void StoriesRow::refreshSegments() { + Expects(_unreadCount <= _count); + Expects(_count > 0); + + auto segments = std::vector(); + const auto add = [&](bool unread) { + segments.push_back({ + .brush = unread ? _unread : st::dialogsUnreadBgMuted->b, + .width = (unread + ? st::dialogsStoriesFull.lineTwice / 2. + : st::dialogsStoriesFull.lineReadTwice / 2.), + }); + }; + segments.reserve(_count); + for (auto i = 0, count = _count - _unreadCount; i != count; ++i) { + add(false); + } + for (auto i = 0; i != _unreadCount; ++i) { + add(true); + } + setCustomizedCheckSegments(std::move(segments)); +} + +StoriesController::StoriesController( + not_null window, + rpl::producer content, + Fn open, + Fn loadMore) +: _window(window) +, _content(std::move(content)) +, _open(std::move(open)) +, _loadMore(std::move(loadMore)) { + const auto createGradient = [=] { + auto gradient = QLinearGradient( + QPoint(10, 0), + QPoint(0, 10)); + gradient.setStops({ + { 0., st::groupCallLive1->c }, + { 1., st::groupCallMuted1->c }, + }); + _unread = QBrush(gradient); + }; + createGradient(); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + createGradient(); + for (auto i = 0, count = int(delegate()->peerListFullRowsCount()) + ; i != count + ; ++i) { + const auto row = delegate()->peerListRowAt(i).get(); + static_cast(row)->updateGradient(_unread); + } + }, _lifetime); +} + +Main::Session &StoriesController::session() const { + return _window->session(); +} + +void StoriesController::prepare() { + if (_loadMore) { + _loadMore(); + } + std::move( + _content + ) | rpl::start_with_next([=](Content content) { + refresh(content); + }, _lifetime); +} + +void StoriesController::loadMoreRows() { + if (_loadMore) { + _loadMore(); + } +} + +void StoriesController::rowClicked(not_null row) { + if (_open) { + _open(row->id()); + } +} + +base::unique_qptr StoriesController::rowContextMenu( + QWidget *parent, + not_null row) { + auto result = base::make_unique_q(parent); + + Dialogs::Stories::FillSourceMenu(_window, { + .id = row->id(), + .callback = Ui::Menu::CreateAddActionCallback(result.get()), + }); + + if (result->empty()) { + return nullptr; + } + return result; +} + +void StoriesController::refresh(const Content &content) { + const auto session = &_window->session(); + const auto positive = _positionPositive = !_positionPositive; + auto position = positive ? 1 : -int(content.elements.size()); + for (const auto &element : content.elements) { + if (const auto row = delegate()->peerListFindRow(element.id)) { + static_cast(row)->position = position; + static_cast(row)->applySegments(element); + } else { + auto added = std::make_unique( + session, + _unread, + element, + position); + const auto raw = added.get(); + delegate()->peerListAppendRow(std::move(added)); + delegate()->peerListSetRowChecked(raw, true); + raw->applySegments(element); + } + ++position; + } + auto count = delegate()->peerListFullRowsCount(); + for (auto i = 0; i != count;) { + const auto row = delegate()->peerListRowAt(i); + const auto position = static_cast(row.get())->position; + if (positive ? (position > 0) : (position < 0)) { + ++i; + } else { + delegate()->peerListRemoveRow(row); + --count; + } + } + delegate()->peerListSortRows([]( + const PeerListRow &a, + const PeerListRow &b) { + return static_cast(a).position + < static_cast(b).position; + }); + delegate()->peerListRefreshRows(); +} + +[[nodiscard]] object_ptr PrepareHiddenStoriesList( + not_null box, + not_null sessionController, + rpl::producer mode) { + auto result = object_ptr>( + box, + object_ptr(box)); + const auto container = result->entity(); + const auto stories = &sessionController->session().data().stories(); + + container->add(CreatePeerListSectionSubtitle( + container, + tr::lng_contacts_hidden_stories())); + + auto &lifetime = container->lifetime(); + auto list = Dialogs::Stories::ContentForSession( + &sessionController->session(), + Data::StorySourcesList::Hidden + ) | rpl::start_spawning(lifetime); + const auto delegate = lifetime.make_state< + PeerListContentDelegateSimple + >(); + const auto open = [=](uint64 id) { + sessionController->openPeerStories( + PeerId(int64(id)), + Data::StorySourcesList::Hidden); + }; + const auto loadMore = [=] { + stories->loadMore(Data::StorySourcesList::Hidden); + }; + const auto controller = lifetime.make_state( + sessionController, + rpl::duplicate( + list + ) | rpl::filter([](const Dialogs::Stories::Content &list) { + return !list.elements.empty(); + }), + open, + loadMore); + controller->setStyleOverrides(&st::contactsWithStories); + const auto content = container->add(object_ptr( + container, + controller)); + delegate->setContent(content); + controller->setDelegate(delegate); + + container->add(CreatePeerListSectionSubtitle( + container, + rpl::conditional( + std::move( + mode + ) | rpl::map( + rpl::mappers::_1 == ContactsBoxController::SortMode::Online + ), + tr::lng_contacts_by_online(), + tr::lng_contacts_by_name()))); + + stories->incrementPreloadingHiddenSources(); + lifetime.add([=] { + stories->decrementPreloadingHiddenSources(); + }); + + result->toggleOn(rpl::duplicate( + list + ) | rpl::map([](const Dialogs::Stories::Content &list) { + return !list.elements.empty(); + })); + result->finishAnimating(); + + return result; +} + } // namespace object_ptr PrepareContactsBox( @@ -55,14 +350,14 @@ object_ptr PrepareContactsBox( using Mode = ContactsBoxController::SortMode; auto controller = std::make_unique( &sessionController->session()); + controller->setStyleOverrides(&st::contactsWithStories); const auto raw = controller.get(); auto init = [=](not_null box) { using namespace Dialogs::Stories; struct State { - List *stories = nullptr; QPointer<::Ui::IconButton> toggleSort; - Mode mode = ContactsBoxController::SortMode::Online; + rpl::variable mode = Mode::Online; ::Ui::Animations::Simple scrollAnimation; }; @@ -73,109 +368,20 @@ object_ptr PrepareContactsBox( tr::lng_profile_add_contact(), [=] { sessionController->showAddContact(); }); state->toggleSort = box->addTopButton(st::contactsSortButton, [=] { - const auto online = (state->mode == Mode::Online); - state->mode = online ? Mode::Alphabet : Mode::Online; - raw->setSortMode(state->mode); + const auto online = (state->mode.current() == Mode::Online); + const auto mode = online ? Mode::Alphabet : Mode::Online; + state->mode = mode; + raw->setSortMode(mode); state->toggleSort->setIconOverride( online ? &st::contactsSortOnlineIcon : nullptr, online ? &st::contactsSortOnlineIconOver : nullptr); }); raw->setSortMode(Mode::Online); - auto list = object_ptr( + box->peerListSetAboveWidget(PrepareHiddenStoriesList( box, - st::dialogsStoriesList, - ContentForSession( - &sessionController->session(), - Data::StorySourcesList::Hidden), - [=] { return state->stories->height() - box->scrollTop(); }); - const auto raw = state->stories = list.data(); - box->peerListSetAboveWidget(object_ptr<::Ui::PaddingWrap<>>( - box, - std::move(list), - style::margins(0, st::membersMarginTop, 0, 0))); - - raw->clicks( - ) | rpl::start_with_next([=](uint64 id) { - sessionController->openPeerStories( - PeerId(int64(id)), - Data::StorySourcesList::Hidden); - }, raw->lifetime()); - - raw->showMenuRequests( - ) | rpl::start_with_next([=](const ShowMenuRequest &request) { - FillSourceMenu(sessionController, request); - }, raw->lifetime()); - - raw->loadMoreRequests( - ) | rpl::start_with_next([=] { - stories->loadMore(Data::StorySourcesList::Hidden); - }, raw->lifetime()); - - const auto defaultScrollTop = [=] { - return std::max(raw->height() - st::dialogsStories.height, 0); - }; - const auto scrollToDefault = [=](bool verytop) { - if (state->scrollAnimation.animating()) { - return; - } - if (verytop) { - //_scroll->verticalScrollBar()->setMinimum(0); - } - state->scrollAnimation.stop(); - auto scrollTop = box->scrollTop(); - const auto scrollTo = verytop ? 0 : defaultScrollTop(); - if (scrollTop == scrollTo) { - return; - } - const auto maxAnimatedDelta = box->height(); - if (scrollTo + maxAnimatedDelta < scrollTop) { - scrollTop = scrollTo + maxAnimatedDelta; - box->scrollToY(scrollTop); - } - - const auto scroll = [=] { - const auto animated = qRound( - state->scrollAnimation.value(scrollTo)); - const auto animatedDelta = animated - scrollTo; - const auto realDelta = box->scrollTop() - scrollTo; - if (realDelta * animatedDelta < 0) { - // We scrolled manually to the other side of target 'scrollTo'. - state->scrollAnimation.stop(); - } else if (std::abs(realDelta) > std::abs(animatedDelta)) { - // We scroll by animation only if it gets us closer to target. - box->scrollToY(animated); - } - }; - - state->scrollAnimation.start( - scroll, - scrollTop, - scrollTo, - st::slideDuration, - anim::sineInOut); - }; - const auto top = box->scrollTop(); - raw->toggleExpandedRequests( - ) | rpl::start_with_next([=](bool expanded) { - if (expanded || box->scrollTop() < defaultScrollTop()) { - scrollToDefault(expanded); - } - }, raw->lifetime()); - - raw->heightValue( - ) | rpl::filter([=] { - return (box->scrollHeight() > 0) - && (defaultScrollTop() > box->scrollTop()); - }) | rpl::start_with_next([=] { - //refreshForDefaultScroll(); - box->scrollToY(defaultScrollTop()); - }, raw->lifetime()); - - stories->incrementPreloadingHiddenSources(); - raw->lifetime().add([=] { - stories->decrementPreloadingHiddenSources(); - }); + sessionController, + state->mode.value())); }; return Box(std::move(controller), std::move(init)); } diff --git a/Telegram/SourceFiles/data/data_stories.cpp b/Telegram/SourceFiles/data/data_stories.cpp index b5dee66591..8f27352a00 100644 --- a/Telegram/SourceFiles/data/data_stories.cpp +++ b/Telegram/SourceFiles/data/data_stories.cpp @@ -37,6 +37,7 @@ constexpr auto kSavedFirstPerPage = 30; constexpr auto kSavedPerPage = 100; constexpr auto kMaxPreloadSources = 10; constexpr auto kStillPreloadFromFirst = 3; +constexpr auto kMaxSegmentsCount = 180; using UpdateFlag = StoryUpdate::Flag; @@ -74,13 +75,15 @@ StoriesSourceInfo StoriesSource::info() const { return { .id = user->id, .last = ids.empty() ? 0 : ids.back().date, - .unread = unread(), - .premium = user->isPremium(), + .count = std::min(int(ids.size()), kMaxSegmentsCount), + .unreadCount = std::min(unreadCount(), kMaxSegmentsCount), + .premium = user->isPremium() ? 1 : 0, }; } -bool StoriesSource::unread() const { - return !ids.empty() && readTill < ids.back().id; +int StoriesSource::unreadCount() const { + const auto i = ids.lower_bound(StoryIdDates{ .id = readTill + 1 }); + return int(end(ids) - i); } StoryIdDates StoriesSource::toOpen() const { @@ -724,7 +727,7 @@ void Stories::sort(StorySourcesList list) { const auto key = int64(info.last) + (info.premium ? (int64(1) << 47) : 0) + ((info.id == changelogSenderId) ? (int64(1) << 47) : 0) - + (info.unread ? (int64(1) << 49) : 0) + + ((info.unreadCount > 0) ? (int64(1) << 49) : 0) + ((info.id == self) ? (int64(1) << 50) : 0); return std::make_pair(key, info.id); }; @@ -895,10 +898,10 @@ void Stories::markAsRead(FullStoryId id, bool viewed) { sendMarkAsReadRequests(); } _markReadPending.emplace(id.peer); - const auto wasUnread = i->second.unread(); + const auto wasUnreadCount = i->second.unreadCount(); i->second.readTill = id.story; - const auto nowUnread = i->second.unread(); - if (wasUnread != nowUnread) { + const auto nowUnreadCount = i->second.unreadCount(); + if (wasUnreadCount != nowUnreadCount) { const auto refreshInList = [&](StorySourcesList list) { auto &sources = _sources[static_cast(list)]; const auto i = ranges::find( @@ -906,7 +909,7 @@ void Stories::markAsRead(FullStoryId id, bool viewed) { id.peer, &StoriesSourceInfo::id); if (i != end(sources)) { - i->unread = nowUnread; + i->unreadCount = nowUnreadCount; sort(list); } }; diff --git a/Telegram/SourceFiles/data/data_stories.h b/Telegram/SourceFiles/data/data_stories.h index 6cb415977e..437a26a7b0 100644 --- a/Telegram/SourceFiles/data/data_stories.h +++ b/Telegram/SourceFiles/data/data_stories.h @@ -41,8 +41,9 @@ struct StoriesIds { struct StoriesSourceInfo { PeerId id = 0; TimeId last = 0; - bool unread = false; - bool premium = false; + int count : 15 = 0; + int unreadCount : 15 = 0; + int premium : 1 = 0; friend inline bool operator==( StoriesSourceInfo, @@ -56,7 +57,7 @@ struct StoriesSource { bool hidden = false; [[nodiscard]] StoriesSourceInfo info() const; - [[nodiscard]] bool unread() const; + [[nodiscard]] int unreadCount() const; [[nodiscard]] StoryIdDates toOpen() const; friend inline bool operator==(StoriesSource, StoriesSource) = default; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp index 243c36b437..329f612961 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_content.cpp @@ -351,8 +351,9 @@ Content State::next() { ? tr::lng_stories_my_name(tr::now) : user->shortName()), .thumbnail = std::move(userpic), - .unread = info.unread, - .skipSmall = user->isSelf(), + .count = info.count, + .unreadCount = info.unreadCount, + .skipSmall = user->isSelf() ? 1 : 0, }); } return result; @@ -423,7 +424,8 @@ rpl::producer LastForPeer(not_null peer) { result.elements.push_back({ .id = uint64(id), .thumbnail = MakeStoryThumbnail(*maybe), - .unread = (id > readTill), + .count = 1, + .unreadCount = (id > readTill) ? 1 : 0, }); } } else if (maybe.error() == Data::NoStory::Unknown) { diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp index b94ef43b1f..23da7e9c86 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.cpp @@ -108,7 +108,8 @@ void List::showContent(Content &&content) { item.element.name = element.name; item.nameCache = QImage(); } - item.element.unread = element.unread; + item.element.count = element.count; + item.element.unreadCount = element.unreadCount; } else { _data.items.emplace_back(Item{ .element = element }); } @@ -129,7 +130,7 @@ List::Summaries List::ComposeSummaries(Data &data) { auto unreadInFirst = 0; auto unreadTotal = 0; for (auto i = skip; i != total; ++i) { - if (data.items[i].element.unread) { + if (data.items[i].element.unreadCount > 0) { ++unreadTotal; if (i < skip + kSmallThumbsShown) { ++unreadInFirst; @@ -162,7 +163,7 @@ List::Summaries List::ComposeSummaries(Data &data) { } if (unreadInFirst > 0 && unreadInFirst == unreadTotal) { for (auto i = skip; i != total; ++i) { - if (data.items[i].element.unread) { + if (data.items[i].element.unreadCount > 0) { append(result.unreadNames.string, i, !--unreadTotal); } } @@ -449,8 +450,8 @@ void List::paintEvent(QPaintEvent *e) { return Single{ x, indexSmall, small, indexFull, full, y }; }; const auto hasUnread = [&](const Single &single) { - return (single.itemSmall && single.itemSmall->element.unread) - || (single.itemFull && single.itemFull->element.unread); + return (single.itemSmall && single.itemSmall->element.unreadCount) + || (single.itemFull && single.itemFull->element.unreadCount); }; const auto enumerate = [&](auto &&paintGradient, auto &&paintOther) { auto nextGradientPainted = false; @@ -513,8 +514,8 @@ void List::paintEvent(QPaintEvent *e) { photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; - const auto smallUnread = small && small->element.unread; - const auto fullUnread = itemFull && itemFull->element.unread; + const auto smallUnread = small && small->element.unreadCount; + const auto fullUnread = itemFull && itemFull->element.unreadCount; const auto unreadOpacity = (smallUnread && fullUnread) ? 1. : smallUnread @@ -550,8 +551,8 @@ void List::paintEvent(QPaintEvent *e) { photo); const auto small = single.itemSmall; const auto itemFull = single.itemFull; - const auto smallUnread = small && small->element.unread; - const auto fullUnread = itemFull && itemFull->element.unread; + const auto smallUnread = small && small->element.unreadCount; + const auto fullUnread = itemFull && itemFull->element.unreadCount; // White circle with possible read gray line. const auto hasReadLine = (itemFull && !fullUnread); diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h index 49391432c0..9bbca05b06 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_stories_list.h @@ -36,8 +36,9 @@ struct Element { uint64 id = 0; QString name; std::shared_ptr thumbnail; - bool unread : 1 = false; - bool skipSmall : 1 = false; + int count : 15 = 0; + int unreadCount : 15 = 0; + int skipSmall : 1 = 0; friend inline bool operator==( const Element &a, diff --git a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp index 15473babc2..b6fc6254f7 100644 --- a/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp +++ b/Telegram/SourceFiles/info/stories/info_stories_inner_widget.cpp @@ -160,7 +160,7 @@ void InnerWidget::createButtons() { self ) | rpl::map([=](Content &&content) { for (auto &element : content.elements) { - element.unread = false; + element.unreadCount = 0; } return std::move(content); }) | rpl::start_spawning(recentWrap->lifetime()); diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp index 74c51e1440..4d2a03f776 100644 --- a/Telegram/SourceFiles/ui/effects/round_checkbox.cpp +++ b/Telegram/SourceFiles/ui/effects/round_checkbox.cpp @@ -389,26 +389,49 @@ void RoundImageCheckbox::paint(Painter &p, int x, int y, int outerWidth) const { } if (selectionLevel > 0) { - const auto radius = _roundingRadius - ? _roundingRadius(_st.imageRadius * 2) - : std::optional(); PainterHighQualityEnabler hq(p); p.setOpacity(std::clamp(selectionLevel, 0., 1.)); p.setBrush(Qt::NoBrush); - const auto pen = QPen( - _fgOverride ? (*_fgOverride) : _st.selectFg->b, - _st.selectWidth); - p.setPen(pen); + const auto segments = int(_segments.size()); const auto rect = style::rtlrect( x, y, _st.imageRadius * 2, _st.imageRadius * 2, outerWidth); - if (!radius) { - p.drawEllipse(rect); + if (segments < 2) { + const auto radius = _roundingRadius + ? _roundingRadius(_st.imageRadius * 2) + : std::optional(); + const auto pen = QPen( + segments ? _segments.front().brush : _st.selectFg->b, + segments ? _segments.front().width : _st.selectWidth); + p.setPen(pen); + if (!radius) { + p.drawEllipse(rect); + } else { + p.drawRoundedRect(rect, *radius, *radius); + } } else { - p.drawRoundedRect(rect, *radius, *radius); + const auto small = 160; + const auto full = arc::kFullLength; + const auto separator = (full > 2 * small * segments) + ? small + : full / (segments * 2); + const auto left = full - (separator * segments); + const auto length = left / float64(segments); + auto start = 0. + (arc::kQuarterLength + (separator / 2)); + for (const auto &segment : ranges::views::reverse(_segments)) { + p.setPen(QPen( + segment.brush, + segment.width, + Qt::SolidLine, + Qt::RoundCap)); + const auto from = int(base::SafeRound(start)); + const auto till = int(base::SafeRound(start + length)); + p.drawArc(rect, from, till - from); + start += length + separator; + } } p.setOpacity(1.); } @@ -474,7 +497,18 @@ void RoundImageCheckbox::prepareWideCache() { } void RoundImageCheckbox::setColorOverride(std::optional fg) { - _fgOverride = fg; + if (fg) { + setCustomizedSegments({ + { .brush = *fg, .width = float64(_st.selectWidth) } + }); + } else { + setCustomizedSegments({}); + } +} + +void RoundImageCheckbox::setCustomizedSegments( + std::vector segments) { + _segments = std::move(segments); } } // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/round_checkbox.h b/Telegram/SourceFiles/ui/effects/round_checkbox.h index 461244f95d..9247c226e7 100644 --- a/Telegram/SourceFiles/ui/effects/round_checkbox.h +++ b/Telegram/SourceFiles/ui/effects/round_checkbox.h @@ -45,8 +45,14 @@ private: }; +struct RoundImageCheckboxSegment { + QBrush brush; + float64 width = 0.; +}; + class RoundImageCheckbox { public: + using Segment = RoundImageCheckboxSegment; using PaintRoundImage = Fn; RoundImageCheckbox( const style::RoundImageCheckbox &st, @@ -58,6 +64,7 @@ public: float64 checkedAnimationRatio() const; void setColorOverride(std::optional fg); + void setCustomizedSegments(std::vector segments); bool checked() const { return _check.checked(); @@ -83,7 +90,8 @@ private: RoundCheckbox _check; - std::optional _fgOverride; + //std::optional _fgOverride; + std::vector _segments; };