Show badges in new tabs.

This commit is contained in:
John Preston 2025-05-27 16:59:38 +04:00
parent 8512154b45
commit 5943052cd1
5 changed files with 197 additions and 30 deletions

View file

@ -93,6 +93,9 @@ struct BadgesState {
friend inline constexpr auto operator<=>( friend inline constexpr auto operator<=>(
BadgesState, BadgesState,
BadgesState) = default; BadgesState) = default;
friend inline constexpr bool operator==(
BadgesState,
BadgesState) = default;
[[nodiscard]] bool empty() const { [[nodiscard]] bool empty() const {
return !unread && !mention && !reaction; return !unread && !mention && !reaction;

View file

@ -180,11 +180,11 @@ void SubsectionTabs::setupSlider(
slider->sectionActivated() | rpl::start_with_next([=](int active) { slider->sectionActivated() | rpl::start_with_next([=](int active) {
if (active >= 0 if (active >= 0
&& active < _slice.size() && active < _slice.size()
&& _active != _slice[active]) { && _active != _slice[active].thread) {
auto params = Window::SectionShow(); auto params = Window::SectionShow();
params.way = Window::SectionShow::Way::ClearStack; params.way = Window::SectionShow::Way::ClearStack;
params.animated = anim::type::instant; params.animated = anim::type::instant;
_controller->showThread(_slice[active], {}, params); _controller->showThread(_slice[active].thread, {}, params);
} }
}, slider->lifetime()); }, slider->lifetime());
@ -209,11 +209,11 @@ void SubsectionTabs::setupSlider(
if (availableFrom < full if (availableFrom < full
&& _beforeSkipped.value_or(0) > 0 && _beforeSkipped.value_or(0) > 0
&& !_slice.empty()) { && !_slice.empty()) {
_around = _slice.front(); _around = _slice.front().thread;
refreshSlice(); refreshSlice();
} else if (availableTill < full) { } else if (availableTill < full) {
if (_afterAvailable > 0) { if (_afterAvailable > 0) {
_around = _slice.back(); _around = _slice.back().thread;
refreshSlice(); refreshSlice();
} else if (!_afterSkipped.has_value()) { } else if (!_afterSkipped.has_value()) {
_loading = true; _loading = true;
@ -232,9 +232,9 @@ void SubsectionTabs::setupSlider(
}; };
auto sections = std::vector<Ui::SubsectionTab>(); auto sections = std::vector<Ui::SubsectionTab>();
auto activeIndex = -1; auto activeIndex = -1;
for (const auto &thread : _slice) { for (const auto &item : _slice) {
const auto index = int(sections.size()); const auto index = int(sections.size());
if (thread == _active) { if (item.thread == _active) {
activeIndex = index; activeIndex = index;
} }
const auto textFg = [=] { const auto textFg = [=] {
@ -243,23 +243,24 @@ void SubsectionTabs::setupSlider(
st::windowActiveTextFg, st::windowActiveTextFg,
slider->buttonActive(slider->buttonAt(index))); slider->buttonActive(slider->buttonAt(index)));
}; };
if (const auto topic = thread->asTopic()) { if (const auto topic = item.thread->asTopic()) {
if (vertical) { if (vertical) {
const auto general = topic->isGeneral();
sections.push_back({ sections.push_back({
.text = { topic->title() }, .text = { item.name },
.userpic = (topic->iconId() .userpic = (item.iconId
? Ui::MakeEmojiThumbnail( ? Ui::MakeEmojiThumbnail(
&topic->owner(), &topic->owner(),
Data::SerializeCustomEmojiId(topic->iconId()), Data::SerializeCustomEmojiId(item.iconId),
paused, paused,
textFg) textFg)
: Ui::MakeEmojiThumbnail( : Ui::MakeEmojiThumbnail(
&topic->owner(), &topic->owner(),
Data::TopicIconEmojiEntity({ Data::TopicIconEmojiEntity({
.title = (topic->isGeneral() .title = (general
? Data::ForumGeneralIconTitle() ? Data::ForumGeneralIconTitle()
: topic->title()), : item.name),
.colorId = (topic->isGeneral() .colorId = (general
? Data::ForumGeneralIconColor( ? Data::ForumGeneralIconColor(
st::windowSubTextFg->c) st::windowSubTextFg->c)
: topic->colorId()), : topic->colorId()),
@ -272,7 +273,7 @@ void SubsectionTabs::setupSlider(
.text = topic->titleWithIcon(), .text = topic->titleWithIcon(),
}); });
} }
} else if (const auto sublist = thread->asSublist()) { } else if (const auto sublist = item.thread->asSublist()) {
const auto peer = sublist->sublistPeer(); const auto peer = sublist->sublistPeer();
if (vertical) { if (vertical) {
sections.push_back({ sections.push_back({
@ -294,6 +295,8 @@ void SubsectionTabs::setupSlider(
.userpic = Ui::MakeAllSubsectionsThumbnail(textFg), .userpic = Ui::MakeAllSubsectionsThumbnail(textFg),
}); });
} }
auto &section = sections.back();
section.badges = item.badges;
} }
auto scrollSavingThread = (Data::Thread*)nullptr; auto scrollSavingThread = (Data::Thread*)nullptr;
@ -309,21 +312,24 @@ void SubsectionTabs::setupSlider(
? slider->lookupSectionPosition(index + 1) ? slider->lookupSectionPosition(index + 1)
: (indexPosition + scrollValue + 1); : (indexPosition + scrollValue + 1);
if (indexPosition <= scrollValue && nextPosition > scrollValue) { if (indexPosition <= scrollValue && nextPosition > scrollValue) {
scrollSavingThread = _sectionsSlice[index]; scrollSavingThread = _sectionsSlice[index].thread;
scrollSavingShift = scrollValue - indexPosition; scrollSavingShift = scrollValue - indexPosition;
break; break;
} }
indexPosition = nextPosition; indexPosition = nextPosition;
} }
scrollSavingIndex = scrollSavingThread scrollSavingIndex = scrollSavingThread
? int(ranges::find(_slice, not_null(scrollSavingThread)) ? int(ranges::find(
- begin(_slice)) _slice,
not_null(scrollSavingThread),
&Item::thread
) - begin(_slice))
: -1; : -1;
if (scrollSavingIndex == _slice.size()) { if (scrollSavingIndex == _slice.size()) {
scrollSavingIndex = -1; scrollSavingIndex = -1;
for (auto index = 0; index != count; ++index) { for (auto index = 0; index != count; ++index) {
const auto thread = _sectionsSlice[index]; const auto thread = _sectionsSlice[index].thread;
if (ranges::contains(_slice, thread)) { if (ranges::contains(_slice, thread, &Item::thread)) {
scrollSavingThread = thread; scrollSavingThread = thread;
scrollSavingShift = scrollValue scrollSavingShift = scrollValue
- slider->lookupSectionPosition(index); - slider->lookupSectionPosition(index);
@ -483,6 +489,7 @@ void SubsectionTabs::setVisible(bool shown) {
} }
void SubsectionTabs::track() { void SubsectionTabs::track() {
using Event = Data::Session::ChatListEntryRefresh;
if (const auto forum = _history->peer->forum()) { if (const auto forum = _history->peer->forum()) {
forum->topicDestroyed( forum->topicDestroyed(
) | rpl::start_with_next([=](not_null<Data::ForumTopic*> topic) { ) | rpl::start_with_next([=](not_null<Data::ForumTopic*> topic) {
@ -491,6 +498,19 @@ void SubsectionTabs::track() {
refreshSlice(); refreshSlice();
} }
}, _lifetime); }, _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()) { } else if (const auto monoforum = _history->peer->monoforum()) {
monoforum->sublistDestroyed( monoforum->sublistDestroyed(
) | rpl::start_with_next([=](not_null<Data::SavedSublist*> sublist) { ) | rpl::start_with_next([=](not_null<Data::SavedSublist*> sublist) {
@ -499,12 +519,29 @@ void SubsectionTabs::track() {
refreshSlice(); refreshSlice();
} }
}, _lifetime); }, _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 { } else {
Unexpected("Peer in SubsectionTabs::track."); Unexpected("Peer in SubsectionTabs::track.");
} }
} }
void SubsectionTabs::refreshSlice() { void SubsectionTabs::refreshSlice() {
_refreshScheduled = false;
const auto forum = _history->peer->forum(); const auto forum = _history->peer->forum();
const auto monoforum = _history->peer->monoforum(); const auto monoforum = _history->peer->monoforum();
Assert(forum || monoforum); Assert(forum || monoforum);
@ -512,15 +549,25 @@ void SubsectionTabs::refreshSlice() {
const auto list = forum const auto list = forum
? forum->topicsList() ? forum->topicsList()
: monoforum->chatsList(); : monoforum->chatsList();
auto slice = std::vector<not_null<Data::Thread*>>(); auto slice = std::vector<Item>();
slice.reserve(_slice.size() + 10);
const auto guard = gsl::finally([&] { const auto guard = gsl::finally([&] {
if (_slice != slice) { if (_slice != slice) {
_slice = std::move(slice); _slice = std::move(slice);
_refreshed.fire({}); _refreshed.fire({});
} }
}); });
const auto push = [&](not_null<Data::Thread*> thread) {
const auto topic = thread->asTopic();
slice.push_back({
.thread = thread,
.badges = thread->chatListBadgesState(),
.iconId = topic ? topic->iconId() : DocumentId(),
.name = thread->chatListName(),
});
};
if (!list) { if (!list) {
slice.push_back(_history); push(_history);
_beforeSkipped = _afterSkipped = 0; _beforeSkipped = _afterSkipped = 0;
_afterAvailable = 0; _afterAvailable = 0;
return; return;
@ -542,13 +589,25 @@ void SubsectionTabs::refreshSlice() {
_afterAvailable = std::max(0, int(chats.end() - till)); _afterAvailable = std::max(0, int(chats.end() - till));
_afterSkipped = list->loaded() ? _afterAvailable : std::optional<int>(); _afterSkipped = list->loaded() ? _afterAvailable : std::optional<int>();
if (from == chats.begin()) { if (from == chats.begin()) {
slice.push_back(_history); push(_history);
} }
for (auto i = from; i != till; ++i) { 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( bool SubsectionTabs::switchTo(
not_null<Data::Thread*> thread, not_null<Data::Thread*> thread,
not_null<Ui::RpWidget*> parent) { not_null<Ui::RpWidget*> parent) {

View file

@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "dialogs/dialogs_common.h"
class History; class History;
namespace Data { namespace Data {
@ -53,12 +55,27 @@ public:
void hide(); void hide();
private: private:
struct Item {
not_null<Data::Thread*> 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 track();
void setupHorizontal(not_null<QWidget*> parent); void setupHorizontal(not_null<QWidget*> parent);
void setupVertical(not_null<QWidget*> parent); void setupVertical(not_null<QWidget*> parent);
void toggleModes(); void toggleModes();
void setVisible(bool shown); void setVisible(bool shown);
void refreshSlice(); void refreshSlice();
void scheduleRefresh();
void loadMore(); void loadMore();
[[nodiscard]] rpl::producer<> dataChanged() const; [[nodiscard]] rpl::producer<> dataChanged() const;
@ -74,8 +91,8 @@ private:
Ui::RpWidget *_vertical = nullptr; Ui::RpWidget *_vertical = nullptr;
Ui::RpWidget *_shadow = nullptr; Ui::RpWidget *_shadow = nullptr;
std::vector<not_null<Data::Thread*>> _slice; std::vector<Item> _slice;
std::vector<not_null<Data::Thread*>> _sectionsSlice; std::vector<Item> _sectionsSlice;
not_null<Data::Thread*> _active; not_null<Data::Thread*> _active;
not_null<Data::Thread*> _around; not_null<Data::Thread*> _around;
@ -83,6 +100,7 @@ private:
int _afterLimit = 0; int _afterLimit = 0;
int _afterAvailable = 0; int _afterAvailable = 0;
bool _loading = false; bool _loading = false;
bool _refreshScheduled = false;
std::optional<int> _beforeSkipped; std::optional<int> _beforeSkipped;
std::optional<int> _afterSkipped; std::optional<int> _afterSkipped;

View file

@ -8,9 +8,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/controls/subsection_tabs_slider.h" #include "ui/controls/subsection_tabs_slider.h"
#include "base/call_delayed.h" #include "base/call_delayed.h"
#include "dialogs/dialogs_three_state_icon.h"
#include "ui/effects/ripple_animation.h" #include "ui/effects/ripple_animation.h"
#include "ui/dynamic_image.h" #include "ui/dynamic_image.h"
#include "ui/unread_badge_paint.h"
#include "styles/style_chat.h" #include "styles/style_chat.h"
#include "styles/style_dialogs.h"
#include "styles/style_filter_icons.h" #include "styles/style_filter_icons.h"
namespace Ui { namespace Ui {
@ -105,6 +108,41 @@ void VerticalButton::paintEvent(QPaintEvent *e) {
.align = style::al_top, .align = style::al_top,
.paused = _delegate->buttonPaused(), .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( HorizontalButton::HorizontalButton(
@ -118,7 +156,28 @@ HorizontalButton::HorizontalButton(
} }
void HorizontalButton::updateSize() { 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() { void HorizontalButton::dataUpdatedHook() {
@ -149,6 +208,36 @@ void HorizontalButton::paintEvent(QPaintEvent *e) {
.availableWidth = _text.maxWidth(), .availableWidth = _text.maxWidth(),
.paused = _delegate->buttonPaused(), .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 } // namespace

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#pragma once #pragma once
#include "dialogs/dialogs_common.h"
#include "ui/round_rect.h" #include "ui/round_rect.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
@ -25,10 +26,7 @@ class SubsectionButton;
struct SubsectionTab { struct SubsectionTab {
TextWithEntities text; TextWithEntities text;
std::shared_ptr<DynamicImage> userpic; std::shared_ptr<DynamicImage> userpic;
int counter = 0; Dialogs::BadgesState badges;
bool muted = false;
bool mention = false;
bool reaciton = false;
}; };
struct SubsectionTabs { struct SubsectionTabs {