Add stories outline to group participants list.

This commit is contained in:
John Preston 2023-07-18 20:10:15 +04:00
parent fad05e8b35
commit f31b40f6ce
9 changed files with 287 additions and 156 deletions

View File

@ -889,9 +889,11 @@ void PeerListRow::createCheckbox(
}
void PeerListRow::setCheckedInternal(bool checked, anim::type animated) {
Expects(_checkbox != nullptr);
Expects(!checked || _checkbox != nullptr);
_checkbox->setChecked(checked, animated);
if (_checkbox) {
_checkbox->setChecked(checked, animated);
}
}
void PeerListRow::setCustomizedCheckSegments(

View File

@ -54,40 +54,6 @@ namespace {
constexpr auto kSortByOnlineThrottle = 3 * crl::time(1000);
constexpr auto kSearchPerPage = 50;
[[nodiscard]] std::vector<Ui::OutlineSegment> PrepareSegments(
int count,
int unread,
const QBrush &unreadBrush) {
Expects(unread <= count);
Expects(count > 0);
auto result = std::vector<Ui::OutlineSegment>();
const auto add = [&](bool unread) {
result.push_back({
.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
.width = (unread
? st::dialogsStoriesFull.lineTwice / 2.
: st::dialogsStoriesFull.lineReadTwice / 2.),
});
};
result.reserve(count);
for (auto i = 0, till = count - unread; i != till; ++i) {
add(false);
}
for (auto i = 0; i != unread; ++i) {
add(true);
}
return result;
}
[[nodiscard]] QBrush CreateStoriesGradient() {
const auto &st = st::contactsWithStories.item;
const auto left = st.photoPosition.x();
const auto top = st.photoPosition.y();
const auto size = st.photoSize;
return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));
}
} // namespace
object_ptr<Ui::BoxContent> PrepareContactsBox(
@ -124,6 +90,39 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
return Box<PeerListBox>(std::move(controller), std::move(init));
}
QBrush PeerListStoriesGradient(const style::PeerList &st) {
const auto left = st.item.photoPosition.x();
const auto top = st.item.photoPosition.y();
const auto size = st.item.photoSize;
return Ui::UnreadStoryOutlineGradient(QRectF(left, top, size, size));
}
std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
int count,
int unread,
const QBrush &unreadBrush) {
Expects(unread <= count);
Expects(count > 0);
auto result = std::vector<Ui::OutlineSegment>();
const auto add = [&](bool unread) {
result.push_back({
.brush = unread ? unreadBrush : st::dialogsUnreadBgMuted->b,
.width = (unread
? st::dialogsStoriesFull.lineTwice / 2.
: st::dialogsStoriesFull.lineReadTwice / 2.),
});
};
result.reserve(count);
for (auto i = 0, till = count - unread; i != till; ++i) {
add(false);
}
for (auto i = 0; i != unread; ++i) {
add(true);
}
return result;
}
void PeerListRowWithLink::setActionLink(const QString &action) {
_action = action;
refreshActionLink();
@ -359,6 +358,115 @@ bool ChatsListBoxController::appendRow(not_null<History*> history) {
return false;
}
PeerListStories::PeerListStories(
not_null<PeerListController*> controller,
not_null<Main::Session*> session)
: _controller(controller)
, _session(session) {
}
void PeerListStories::updateColors() {
for (auto i = begin(_counts); i != end(_counts); ++i) {
if (const auto row = _delegate->peerListFindRow(i->first)) {
if (i->second.count >= 0 && i->second.unread >= 0) {
applyForRow(row, i->second.count, i->second.unread, true);
}
}
}
}
void PeerListStories::updateFor(
uint64 id,
int count,
int unread) {
if (const auto row = _delegate->peerListFindRow(id)) {
applyForRow(row, count, unread);
_delegate->peerListUpdateRow(row);
}
}
void PeerListStories::process(not_null<PeerListRow*> row) {
const auto user = row->peer()->asUser();
if (!user) {
return;
}
const auto stories = &_session->data().stories();
const auto source = stories->source(user->id);
const auto count = source
? int(source->ids.size())
: user->hasActiveStories()
? 1
: 0;
const auto unread = source
? source->info().unreadCount
: user->hasUnreadStories()
? 1
: 0;
applyForRow(row, count, unread, true);
}
bool PeerListStories::handleClick(not_null<PeerData*> peer) {
const auto point = _delegate->peerListLastRowMousePosition();
const auto &st = _controller->listSt()->item;
if (point && point->x() < st.photoPosition.x() + st.photoSize) {
if (const auto window = peer->session().tryResolveWindow()) {
if (const auto user = peer->asUser()) {
if (user->hasActiveStories()) {
window->openPeerStories(peer->id);
return true;
}
}
}
}
return false;
}
void PeerListStories::prepare(not_null<PeerListDelegate*> delegate) {
_delegate = delegate;
_unreadBrush = PeerListStoriesGradient(*_controller->listSt());
style::PaletteChanged() | rpl::start_with_next([=] {
_unreadBrush = PeerListStoriesGradient(*_controller->listSt());
updateColors();
}, _lifetime);
_session->changes().peerUpdates(
Data::PeerUpdate::Flag::StoriesState
) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
const auto id = update.peer->id.value;
if (const auto row = _delegate->peerListFindRow(id)) {
process(row);
}
}, _lifetime);
const auto stories = &_session->data().stories();
stories->sourceChanged() | rpl::start_with_next([=](PeerId id) {
const auto source = stories->source(id);
const auto info = source
? source->info()
: Data::StoriesSourceInfo();
updateFor(id.value, info.count, info.unreadCount);
}, _lifetime);
}
void PeerListStories::applyForRow(
not_null<PeerListRow*> row,
int count,
int unread,
bool force) {
auto &counts = _counts[row->id()];
if (!force && counts.count == count && counts.unread == unread) {
return;
}
counts.count = count;
counts.unread = unread;
_delegate->peerListSetRowChecked(row, count > 0);
if (count > 0) {
row->setCustomizedCheckSegments(
PeerListStoriesSegments(count, unread, _unreadBrush));
}
}
ContactsBoxController::ContactsBoxController(
not_null<Main::Session*> session)
: ContactsBoxController(
@ -385,31 +493,8 @@ void ContactsBoxController::prepare() {
prepareViewHook();
if (_storiesShown) {
_storiesUnread = CreateStoriesGradient();
style::PaletteChanged() | rpl::start_with_next([=] {
_storiesUnread = CreateStoriesGradient();
for (auto &entry : _storiesCounts) {
entry.second.count = entry.second.unread = -1;
}
updateStories();
}, lifetime());
const auto stories = &session().data().stories();
rpl::merge(
rpl::single(rpl::empty),
stories->sourcesChanged(Data::StorySourcesList::NotHidden),
stories->sourcesChanged(Data::StorySourcesList::Hidden)
) | rpl::start_with_next([=] {
updateStories();
}, lifetime());
stories->sourceChanged() | rpl::start_with_next([=](PeerId id) {
const auto source = stories->source(id);
const auto info = source
? source->info()
: Data::StoriesSourceInfo();
updateStoriesFor(id.value, info.count, info.unreadCount);
}, lifetime());
if (_stories) {
_stories->prepare(delegate());
}
session().data().contactsLoaded().value(
@ -456,16 +541,10 @@ std::unique_ptr<PeerListRow> ContactsBoxController::createSearchRow(
void ContactsBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto peer = row->peer();
if (const auto window = peer->session().tryResolveWindow()) {
if (_storiesShown) {
const auto point = delegate()->peerListLastRowMousePosition();
const auto &st = st::contactsWithStories.item;
if (point && point->x() < st.photoPosition.x() + st.photoSize) {
window->openPeerStories(peer->id);
return;
}
}
window->showPeerHistory(row->peer());
if (_stories && _stories->handleClick(peer)) {
return;
} else if (const auto window = peer->session().tryResolveWindow()) {
window->showPeerHistory(peer);
}
}
@ -491,52 +570,7 @@ void ContactsBoxController::setSortMode(SortMode mode) {
}
void ContactsBoxController::setStoriesShown(bool shown) {
_storiesShown = shown;
}
void ContactsBoxController::updateStories() {
const auto stories = &_session->data().stories();
const auto &a = stories->sources(Data::StorySourcesList::NotHidden);
const auto &b = stories->sources(Data::StorySourcesList::Hidden);
auto checked = base::flat_set<PeerListRowId>();
for (const auto &info : ranges::views::concat(a, b)) {
const auto id = info.id.value;
checked.emplace(id);
updateStoriesFor(id, info.count, info.unreadCount);
}
for (auto i = begin(_storiesCounts); i != end(_storiesCounts); ++i) {
if (i->second.count && !checked.contains(i->first)) {
updateStoriesFor(i->first, 0, 0);
}
}
}
void ContactsBoxController::updateStoriesFor(
uint64 id,
int count,
int unread) {
if (const auto row = delegate()->peerListFindRow(id)) {
applyRowStories(row, count, unread);
delegate()->peerListUpdateRow(row);
}
}
void ContactsBoxController::applyRowStories(
not_null<PeerListRow*> row,
int count,
int unread,
bool force) {
auto &counts = _storiesCounts[row->id()];
if (!force && counts.count == count && counts.unread == unread) {
return;
}
counts.count = count;
counts.unread = unread;
delegate()->peerListSetRowChecked(row, count > 0);
if (count > 0) {
row->setCustomizedCheckSegments(
PrepareSegments(count, unread, _storiesUnread));
}
_stories = std::make_unique<PeerListStories>(this, _session);
}
void ContactsBoxController::sort() {
@ -586,12 +620,8 @@ bool ContactsBoxController::appendRow(not_null<UserData*> user) {
if (auto row = createRow(user)) {
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (_storiesShown) {
const auto stories = &session().data().stories();
if (const auto source = stories->source(user->id)) {
const auto info = source->info();
applyRowStories(raw, info.count, info.unreadCount, true);
}
if (_stories) {
_stories->process(raw);
}
return true;
}

View File

@ -20,12 +20,21 @@ class Forum;
class ForumTopic;
} // namespace Data
namespace Ui {
struct OutlineSegment;
} // namespace Ui
namespace Window {
class SessionController;
} // namespace Window
[[nodiscard]] object_ptr<Ui::BoxContent> PrepareContactsBox(
not_null<Window::SessionController*> sessionController);
[[nodiscard]] QBrush PeerListStoriesGradient(const style::PeerList &st);
[[nodiscard]] std::vector<Ui::OutlineSegment> PeerListStoriesSegments(
int count,
int unread,
const QBrush &unreadBrush);
class PeerListRowWithLink : public PeerListRow {
public:
@ -116,6 +125,41 @@ private:
};
class PeerListStories final {
public:
PeerListStories(
not_null<PeerListController*> controller,
not_null<Main::Session*> session);
void prepare(not_null<PeerListDelegate*> delegate);
void process(not_null<PeerListRow*> row);
bool handleClick(not_null<PeerData*> peer);
private:
struct Counts {
int count = 0;
int unread = 0;
};
void updateColors();
void updateFor(uint64 id, int count, int unread);
void applyForRow(
not_null<PeerListRow*> row,
int count,
int unread,
bool force = false);
const not_null<PeerListController*> _controller;
const not_null<Main::Session*> _session;
PeerListDelegate *_delegate = nullptr;
QBrush _unreadBrush;
base::flat_map<uint64, Counts> _counts;
rpl::lifetime _lifetime;
};
class ContactsBoxController : public PeerListController {
public:
explicit ContactsBoxController(not_null<Main::Session*> session);
@ -129,7 +173,7 @@ public:
not_null<PeerData*> peer) override final;
void rowClicked(not_null<PeerListRow*> row) override;
bool trackSelectedList() override {
return !_storiesShown;
return !_stories;
}
enum class SortMode {
@ -147,32 +191,19 @@ protected:
}
private:
struct StoriesCount {
int count = 0;
int unread = 0;
};
void sort();
void sortByName();
void sortByOnline();
void rebuildRows();
void updateStories();
void checkForEmptyRows();
bool appendRow(not_null<UserData*> user);
void updateStoriesFor(uint64 id, int count, int unread);
void applyRowStories(
not_null<PeerListRow*> row,
int count,
int unread,
bool force = false);
const not_null<Main::Session*> _session;
SortMode _sortMode = SortMode::Alphabet;
base::Timer _sortByOnlineTimer;
rpl::lifetime _sortByOnlineLifetime;
QBrush _storiesUnread;
base::flat_map<uint64, StoriesCount> _storiesCounts;
bool _storiesShown = false;
std::unique_ptr<PeerListStories> _stories;
};

View File

@ -25,11 +25,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "dialogs/dialogs_indexed_list.h"
#include "data/data_peer_values.h"
#include "data/data_session.h"
#include "data/data_stories.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_user.h"
#include "data/data_changes.h"
#include "base/unixtime.h"
#include "ui/effects/outline_segments.h"
#include "ui/widgets/popup_menu.h"
#include "ui/ui_utility.h"
#include "info/profile/info_profile_values.h"
@ -900,7 +902,11 @@ void ParticipantsBoxController::setupListChangeViewers() {
return (row.peer() == user);
});
} else if (auto row = createRow(user)) {
const auto raw = row.get();
delegate()->peerListPrependRow(std::move(row));
if (_stories) {
_stories->process(raw);
}
refreshRows();
if (_onlineSorter) {
_onlineSorter->sort();
@ -1160,8 +1166,14 @@ void ParticipantsBoxController::restoreState(
loadMoreRows();
}
PeerListController::restoreState(std::move(state));
if (delegate()->peerListFullRowsCount() > 0 || _allLoaded) {
const auto count = delegate()->peerListFullRowsCount();
if (count > 0 || _allLoaded) {
refreshDescription();
if (_stories) {
for (auto i = 0; i != count; ++i) {
_stories->process(delegate()->peerListRowAt(i));
}
}
}
if (_onlineSorter) {
_onlineSorter->sort();
@ -1177,14 +1189,21 @@ rpl::producer<int> ParticipantsBoxController::fullCountValue() const {
return _fullCountValue.value();
}
void ParticipantsBoxController::setStoriesShown(bool shown) {
_stories = std::make_unique<PeerListStories>(
this,
&_navigation->session());
}
void ParticipantsBoxController::prepare() {
auto title = [&] {
switch (_role) {
case Role::Admins: return tr::lng_channel_admins();
case Role::Profile:
case Role::Members: return (_peer->isChannel() && !_peer->isMegagroup()
? tr::lng_profile_subscribers_section()
: tr::lng_profile_participants_section());
case Role::Members:
return ((_peer->isChannel() && !_peer->isMegagroup())
? tr::lng_profile_subscribers_section()
: tr::lng_profile_participants_section());
case Role::Restricted: return tr::lng_exceptions_list_title();
case Role::Kicked: return tr::lng_removed_list_title();
}
@ -1207,6 +1226,10 @@ void ParticipantsBoxController::prepare() {
setDescriptionText(tr::lng_contacts_loading(tr::now));
setSearchNoResultsText(tr::lng_blocked_list_not_found(tr::now));
if (_stories) {
_stories->prepare(delegate());
}
if (_role == Role::Profile) {
auto visible = _peer->isMegagroup()
? Info::Profile::CanViewParticipantsValue(_peer->asMegagroup())
@ -1319,7 +1342,11 @@ void ParticipantsBoxController::rebuildChatParticipants(
}
for (const auto &user : participants) {
if (auto row = createRow(user)) {
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (_stories) {
_stories->process(raw);
}
}
}
_onlineSorter->sort();
@ -1373,7 +1400,11 @@ void ParticipantsBoxController::rebuildChatAdmins(
}
for (const auto user : list) {
if (auto row = createRow(user)) {
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (_stories) {
_stories->process(raw);
}
}
}
@ -1543,6 +1574,11 @@ bool ParticipantsBoxController::feedMegagroupLastParticipants() {
void ParticipantsBoxController::rowClicked(not_null<PeerListRow*> row) {
const auto participant = row->peer();
const auto user = participant->asUser();
if (_stories && _stories->handleClick(participant)) {
return;
}
if (_role == Role::Admins) {
Assert(user != nullptr);
showAdmin(user);
@ -1890,7 +1926,11 @@ bool ParticipantsBoxController::appendRow(not_null<PeerData*> participant) {
recomputeTypeFor(participant);
return false;
} else if (auto row = createRow(participant)) {
const auto raw = row.get();
delegate()->peerListAppendRow(std::move(row));
if (_stories) {
_stories->process(raw);
}
if (_role != Role::Kicked) {
setDescriptionText(QString());
}
@ -1906,10 +1946,17 @@ bool ParticipantsBoxController::prependRow(not_null<PeerData*> participant) {
if (_role == Role::Admins) {
// Perhaps we've added a new admin from search.
delegate()->peerListPrependRowFromSearchResult(row);
if (_stories) {
_stories->process(row);
}
}
return false;
} else if (auto row = createRow(participant)) {
const auto raw = row.get();
delegate()->peerListPrependRow(std::move(row));
if (_stories) {
_stories->process(raw);
}
if (_role != Role::Kicked) {
setDescriptionText(QString());
}

View File

@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/weak_ptr.h"
#include "info/profile/info_profile_members_controllers.h"
class PeerListStories;
struct ChatAdminRightsInfo;
struct ChatRestrictionsInfo;
@ -174,6 +175,9 @@ public:
QWidget *parent,
not_null<PeerListRow*> row) override;
void loadMoreRows() override;
bool trackSelectedList() override {
return !_stories;
}
void peerListSearchAddRow(not_null<PeerData*> peer) override;
std::unique_ptr<PeerListRow> createSearchRow(
@ -187,6 +191,8 @@ public:
[[nodiscard]] rpl::producer<int> onlineCountValue() const;
[[nodiscard]] rpl::producer<int> fullCountValue() const;
void setStoriesShown(bool shown);
protected:
// Allow child controllers not providing navigation.
// This is their responsibility to override all methods that use it.
@ -288,6 +294,8 @@ private:
Ui::BoxPointer _addBox;
QPointer<Ui::BoxContent> _editParticipantBox;
std::unique_ptr<PeerListStories> _stories;
};
// Members, banned and restricted users server side search.

View File

@ -85,26 +85,27 @@ struct PeerUpdate {
SupportInfo = (1ULL << 23),
IsBot = (1ULL << 24),
EmojiStatus = (1ULL << 25),
StoriesState = (1ULL << 26),
// For chats and channels
InviteLinks = (1ULL << 26),
Members = (1ULL << 27),
Admins = (1ULL << 28),
BannedUsers = (1ULL << 29),
Rights = (1ULL << 30),
PendingRequests = (1ULL << 31),
Reactions = (1ULL << 32),
InviteLinks = (1ULL << 27),
Members = (1ULL << 28),
Admins = (1ULL << 29),
BannedUsers = (1ULL << 30),
Rights = (1ULL << 31),
PendingRequests = (1ULL << 32),
Reactions = (1ULL << 33),
// For channels
ChannelAmIn = (1ULL << 33),
StickersSet = (1ULL << 34),
ChannelLinkedChat = (1ULL << 35),
ChannelLocation = (1ULL << 36),
Slowmode = (1ULL << 37),
GroupCall = (1ULL << 38),
ChannelAmIn = (1ULL << 34),
StickersSet = (1ULL << 35),
ChannelLinkedChat = (1ULL << 36),
ChannelLocation = (1ULL << 37),
Slowmode = (1ULL << 38),
GroupCall = (1ULL << 39),
// For iteration
LastUsedBit = (1ULL << 38),
LastUsedBit = (1ULL << 39),
};
using Flags = base::flags<Flag>;
friend inline constexpr auto is_flag_type(Flag) { return true; }

View File

@ -155,6 +155,7 @@ void UserData::setStoriesState(StoriesState state) {
if (const auto history = owner().historyLoaded(this)) {
history->updateChatListEntryPostponed();
}
session().changes().peerUpdated(this, UpdateFlag::StoriesState);
}
}

View File

@ -488,6 +488,15 @@ infoMembersList: PeerList(defaultPeerList) {
photoPosition: point(18px, 6px);
namePosition: point(79px, 11px);
statusPosition: point(79px, 31px);
checkbox: RoundImageCheckbox(defaultPeerListCheckbox) {
selectExtendTwice: 1px;
imageRadius: 21px;
imageSmallRadius: 19px;
check: RoundCheckbox(defaultPeerListCheck) {
size: 0px;
}
}
nameFgChecked: contactsNameFg;
}
}
infoMembersButtonPosition: point(12px, 0px);

View File

@ -50,6 +50,7 @@ Members::Members(
, _controller(controller)
, _peer(_controller->key().peer())
, _listController(CreateMembersController(controller, _peer)) {
_listController->setStoriesShown(true);
setupHeader();
setupList();
setContent(_list.data());
@ -232,6 +233,7 @@ void Members::setupButtons() {
void Members::setupList() {
auto topSkip = _header ? _header->height() : 0;
_listController->setStyleOverrides(&st::infoMembersList);
_listController->setStoriesShown(true);
_list = object_ptr<ListWidget>(
this,
_listController.get());