diff --git a/Telegram/SourceFiles/dialogs/dialogs_common.h b/Telegram/SourceFiles/dialogs/dialogs_common.h index c1e1cd755d..34b844d296 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_common.h +++ b/Telegram/SourceFiles/dialogs/dialogs_common.h @@ -93,6 +93,9 @@ struct BadgesState { friend inline constexpr auto operator<=>( BadgesState, BadgesState) = default; + friend inline constexpr bool operator==( + BadgesState, + BadgesState) = default; [[nodiscard]] bool empty() const { return !unread && !mention && !reaction; diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp index a4b4e64700..932504952b 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.cpp @@ -180,11 +180,11 @@ void SubsectionTabs::setupSlider( slider->sectionActivated() | rpl::start_with_next([=](int active) { if (active >= 0 && active < _slice.size() - && _active != _slice[active]) { + && _active != _slice[active].thread) { auto params = Window::SectionShow(); params.way = Window::SectionShow::Way::ClearStack; params.animated = anim::type::instant; - _controller->showThread(_slice[active], {}, params); + _controller->showThread(_slice[active].thread, {}, params); } }, slider->lifetime()); @@ -209,11 +209,11 @@ void SubsectionTabs::setupSlider( if (availableFrom < full && _beforeSkipped.value_or(0) > 0 && !_slice.empty()) { - _around = _slice.front(); + _around = _slice.front().thread; refreshSlice(); } else if (availableTill < full) { if (_afterAvailable > 0) { - _around = _slice.back(); + _around = _slice.back().thread; refreshSlice(); } else if (!_afterSkipped.has_value()) { _loading = true; @@ -232,9 +232,9 @@ void SubsectionTabs::setupSlider( }; auto sections = std::vector(); auto activeIndex = -1; - for (const auto &thread : _slice) { + for (const auto &item : _slice) { const auto index = int(sections.size()); - if (thread == _active) { + if (item.thread == _active) { activeIndex = index; } const auto textFg = [=] { @@ -243,23 +243,24 @@ void SubsectionTabs::setupSlider( st::windowActiveTextFg, slider->buttonActive(slider->buttonAt(index))); }; - if (const auto topic = thread->asTopic()) { + if (const auto topic = item.thread->asTopic()) { if (vertical) { + const auto general = topic->isGeneral(); sections.push_back({ - .text = { topic->title() }, - .userpic = (topic->iconId() + .text = { item.name }, + .userpic = (item.iconId ? Ui::MakeEmojiThumbnail( &topic->owner(), - Data::SerializeCustomEmojiId(topic->iconId()), + Data::SerializeCustomEmojiId(item.iconId), paused, textFg) : Ui::MakeEmojiThumbnail( &topic->owner(), Data::TopicIconEmojiEntity({ - .title = (topic->isGeneral() + .title = (general ? Data::ForumGeneralIconTitle() - : topic->title()), - .colorId = (topic->isGeneral() + : item.name), + .colorId = (general ? Data::ForumGeneralIconColor( st::windowSubTextFg->c) : topic->colorId()), @@ -272,7 +273,7 @@ void SubsectionTabs::setupSlider( .text = topic->titleWithIcon(), }); } - } else if (const auto sublist = thread->asSublist()) { + } else if (const auto sublist = item.thread->asSublist()) { const auto peer = sublist->sublistPeer(); if (vertical) { sections.push_back({ @@ -294,6 +295,8 @@ void SubsectionTabs::setupSlider( .userpic = Ui::MakeAllSubsectionsThumbnail(textFg), }); } + auto §ion = sections.back(); + section.badges = item.badges; } auto scrollSavingThread = (Data::Thread*)nullptr; @@ -309,21 +312,24 @@ void SubsectionTabs::setupSlider( ? slider->lookupSectionPosition(index + 1) : (indexPosition + scrollValue + 1); if (indexPosition <= scrollValue && nextPosition > scrollValue) { - scrollSavingThread = _sectionsSlice[index]; + scrollSavingThread = _sectionsSlice[index].thread; scrollSavingShift = scrollValue - indexPosition; break; } indexPosition = nextPosition; } scrollSavingIndex = scrollSavingThread - ? int(ranges::find(_slice, not_null(scrollSavingThread)) - - begin(_slice)) + ? int(ranges::find( + _slice, + not_null(scrollSavingThread), + &Item::thread + ) - begin(_slice)) : -1; if (scrollSavingIndex == _slice.size()) { scrollSavingIndex = -1; for (auto index = 0; index != count; ++index) { - const auto thread = _sectionsSlice[index]; - if (ranges::contains(_slice, thread)) { + const auto thread = _sectionsSlice[index].thread; + if (ranges::contains(_slice, thread, &Item::thread)) { scrollSavingThread = thread; scrollSavingShift = scrollValue - slider->lookupSectionPosition(index); @@ -483,6 +489,7 @@ void SubsectionTabs::setVisible(bool shown) { } void SubsectionTabs::track() { + using Event = Data::Session::ChatListEntryRefresh; if (const auto forum = _history->peer->forum()) { forum->topicDestroyed( ) | rpl::start_with_next([=](not_null topic) { @@ -491,6 +498,19 @@ void SubsectionTabs::track() { refreshSlice(); } }, _lifetime); + + forum->topicsList()->unreadStateChanges( + ) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + + forum->owner().chatListEntryRefreshes( + ) | rpl::filter([=](const Event &event) { + const auto topic = event.filterId ? nullptr : event.key.topic(); + return (topic && topic->forum() == forum); + }) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); } else if (const auto monoforum = _history->peer->monoforum()) { monoforum->sublistDestroyed( ) | rpl::start_with_next([=](not_null sublist) { @@ -499,12 +519,29 @@ void SubsectionTabs::track() { refreshSlice(); } }, _lifetime); + + monoforum->chatsList()->unreadStateChanges( + ) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); + + monoforum->owner().chatListEntryRefreshes( + ) | rpl::filter([=](const Event &event) { + const auto sublist = event.filterId + ? nullptr + : event.key.sublist(); + return (sublist && sublist->parent() == monoforum); + }) | rpl::start_with_next([=] { + scheduleRefresh(); + }, _lifetime); } else { Unexpected("Peer in SubsectionTabs::track."); } } void SubsectionTabs::refreshSlice() { + _refreshScheduled = false; + const auto forum = _history->peer->forum(); const auto monoforum = _history->peer->monoforum(); Assert(forum || monoforum); @@ -512,15 +549,25 @@ void SubsectionTabs::refreshSlice() { const auto list = forum ? forum->topicsList() : monoforum->chatsList(); - auto slice = std::vector>(); + auto slice = std::vector(); + slice.reserve(_slice.size() + 10); const auto guard = gsl::finally([&] { if (_slice != slice) { _slice = std::move(slice); _refreshed.fire({}); } }); + const auto push = [&](not_null thread) { + const auto topic = thread->asTopic(); + slice.push_back({ + .thread = thread, + .badges = thread->chatListBadgesState(), + .iconId = topic ? topic->iconId() : DocumentId(), + .name = thread->chatListName(), + }); + }; if (!list) { - slice.push_back(_history); + push(_history); _beforeSkipped = _afterSkipped = 0; _afterAvailable = 0; return; @@ -542,13 +589,25 @@ void SubsectionTabs::refreshSlice() { _afterAvailable = std::max(0, int(chats.end() - till)); _afterSkipped = list->loaded() ? _afterAvailable : std::optional(); if (from == chats.begin()) { - slice.push_back(_history); + push(_history); } for (auto i = from; i != till; ++i) { - slice.push_back((*i)->thread()); + push((*i)->thread()); } } +void SubsectionTabs::scheduleRefresh() { + if (_refreshScheduled) { + return; + } + _refreshScheduled = true; + InvokeQueued(_shadow, [=] { + if (_refreshScheduled) { + refreshSlice(); + } + }); +} + bool SubsectionTabs::switchTo( not_null thread, not_null parent) { diff --git a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h index 65da19a8b4..d198a2fd79 100644 --- a/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h +++ b/Telegram/SourceFiles/history/view/history_view_subsection_tabs.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "dialogs/dialogs_common.h" + class History; namespace Data { @@ -53,12 +55,27 @@ public: void hide(); private: + struct Item { + not_null thread; + Dialogs::BadgesState badges; + DocumentId iconId = 0; + QString name; + + friend inline constexpr auto operator<=>( + const Item &, + const Item &) = default; + friend inline constexpr bool operator==( + const Item &, + const Item &) = default; + }; + void track(); void setupHorizontal(not_null parent); void setupVertical(not_null parent); void toggleModes(); void setVisible(bool shown); void refreshSlice(); + void scheduleRefresh(); void loadMore(); [[nodiscard]] rpl::producer<> dataChanged() const; @@ -74,8 +91,8 @@ private: Ui::RpWidget *_vertical = nullptr; Ui::RpWidget *_shadow = nullptr; - std::vector> _slice; - std::vector> _sectionsSlice; + std::vector _slice; + std::vector _sectionsSlice; not_null _active; not_null _around; @@ -83,6 +100,7 @@ private: int _afterLimit = 0; int _afterAvailable = 0; bool _loading = false; + bool _refreshScheduled = false; std::optional _beforeSkipped; std::optional _afterSkipped; diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp index d6bf6b1ca7..15691b6e21 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.cpp @@ -8,9 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/controls/subsection_tabs_slider.h" #include "base/call_delayed.h" +#include "dialogs/dialogs_three_state_icon.h" #include "ui/effects/ripple_animation.h" #include "ui/dynamic_image.h" +#include "ui/unread_badge_paint.h" #include "styles/style_chat.h" +#include "styles/style_dialogs.h" #include "styles/style_filter_icons.h" namespace Ui { @@ -105,6 +108,41 @@ void VerticalButton::paintEvent(QPaintEvent *e) { .align = style::al_top, .paused = _delegate->buttonPaused(), }); + + const auto &state = _data.badges; + const auto top = _st.userpicTop / 2; + auto right = width() - textLeft; + UnreadBadgeStyle st; + if (state.unread) { + st.muted = state.unreadMuted; + const auto counter = (state.unreadCounter <= 0) + ? QString() + : ((state.mention || state.reaction) + && (state.unreadCounter > 999)) + ? (u"99+"_q) + : (state.unreadCounter > 999999) + ? (u"99999+"_q) + : QString::number(state.unreadCounter); + const auto badge = PaintUnreadBadge(p, counter, right, top, st); + right -= badge.width() + st.padding; + } + if (state.mention || state.reaction) { + UnreadBadgeStyle st; + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.muted = state.mention + ? state.mentionMuted + : state.reactionMuted; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = PaintUnreadBadge(p, counter, right, top, st); + (state.mention + ? st::dialogsUnreadMention.icon + : st::dialogsUnreadReaction.icon).paintInCenter(p, badge); + right -= badge.width() + st.padding + st::dialogsUnreadPadding; + } } HorizontalButton::HorizontalButton( @@ -118,7 +156,28 @@ HorizontalButton::HorizontalButton( } void HorizontalButton::updateSize() { - resize(_st.strictSkip + _text.maxWidth(), _st.height); + auto width = _st.strictSkip + _text.maxWidth(); + + const auto &state = _data.badges; + UnreadBadgeStyle st; + if (state.unread) { + const auto counter = (state.unreadCounter <= 0) + ? QString() + : QString::number(state.unreadCounter); + const auto badge = CountUnreadBadgeSize(counter, st); + width += badge.width() + st.padding; + } + if (state.mention || state.reaction) { + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = CountUnreadBadgeSize(counter, st); + width += badge.width() + st.padding + st::dialogsUnreadPadding; + } + resize(width, _st.height); } void HorizontalButton::dataUpdatedHook() { @@ -149,6 +208,36 @@ void HorizontalButton::paintEvent(QPaintEvent *e) { .availableWidth = _text.maxWidth(), .paused = _delegate->buttonPaused(), }); + + auto right = width() - _st.strictSkip + (_st.strictSkip / 2); + UnreadBadgeStyle st; + const auto &state = _data.badges; + const auto badgeTop = (height() - st.size) / 2; + if (state.unread) { + st.muted = state.unreadMuted; + const auto counter = (state.unreadCounter <= 0) + ? QString() + : QString::number(state.unreadCounter); + const auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st); + right -= badge.width() + st.padding; + } + if (state.mention || state.reaction) { + UnreadBadgeStyle st; + st.sizeId = state.mention + ? UnreadBadgeSize::Dialogs + : UnreadBadgeSize::ReactionInDialogs; + st.muted = state.mention + ? state.mentionMuted + : state.reactionMuted; + st.padding = 0; + st.textTop = 0; + const auto counter = QString(); + const auto badge = PaintUnreadBadge(p, counter, right, badgeTop, st); + (state.mention + ? st::dialogsUnreadMention.icon + : st::dialogsUnreadReaction.icon).paintInCenter(p, badge); + right -= badge.width() + st.padding + st::dialogsUnreadPadding; + } } } // namespace diff --git a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h index 80842d4169..391c51964c 100644 --- a/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h +++ b/Telegram/SourceFiles/ui/controls/subsection_tabs_slider.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "dialogs/dialogs_common.h" #include "ui/round_rect.h" #include "ui/rp_widget.h" #include "ui/widgets/buttons.h" @@ -25,10 +26,7 @@ class SubsectionButton; struct SubsectionTab { TextWithEntities text; std::shared_ptr userpic; - int counter = 0; - bool muted = false; - bool mention = false; - bool reaciton = false; + Dialogs::BadgesState badges; }; struct SubsectionTabs {