Add a tab with "Who Seen" to "Who Reacted" box.

This commit is contained in:
John Preston 2022-01-18 20:46:10 +03:00
parent 74a28ffdf7
commit c8f7a8c795
9 changed files with 252 additions and 137 deletions

View File

@ -51,16 +51,16 @@ inline bool operator==(
struct PeersWithReactions {
std::vector<PeerWithReaction> list;
std::vector<PeerId> read;
int fullReactionsCount = 0;
int fullReadCount = 0;
bool unknown = false;
};
inline bool operator==(
const PeersWithReactions &a,
const PeersWithReactions &b) noexcept {
return (a.fullReactionsCount == b.fullReactionsCount)
&& (a.fullReadCount == b.fullReadCount)
&& (a.list == b.list)
&& (a.read == b.read)
&& (a.unknown == b.unknown);
}
@ -246,14 +246,15 @@ struct State {
}
[[nodiscard]] PeersWithReactions WithEmptyReactions(
const Peers &peers) {
return PeersWithReactions{
Peers &&peers) {
auto result = PeersWithReactions{
.list = peers.list | ranges::views::transform([](PeerId peer) {
return PeerWithReaction{.peer = peer };
}) | ranges::to_vector,
.fullReadCount = int(peers.list.size()),
.unknown = peers.unknown,
};
result.read = std::move(peers.list);
return result;
}
[[nodiscard]] rpl::producer<PeersWithReactions> WhoReactedIds(
@ -322,17 +323,17 @@ struct State {
return rpl::combine(
WhoReactedIds(item, QString(), context),
WhoReadIds(item, context)
) | rpl::map([=](PeersWithReactions reacted, Peers read) {
) | rpl::map([=](PeersWithReactions &&reacted, Peers &&read) {
if (reacted.unknown || read.unknown) {
return PeersWithReactions{ .unknown = true };
}
auto &list = reacted.list;
reacted.fullReadCount = int(read.list.size());
for (const auto &peer : read.list) {
if (!ranges::contains(list, peer, &PeerWithReaction::peer)) {
list.push_back({ .peer = peer });
}
}
reacted.read = std::move(read.list);
return reacted;
});
}
@ -442,6 +443,104 @@ void RegenerateParticipants(not_null<State*> state, int small, int large) {
RegenerateUserpics(state, small, large);
}
rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
const QString &reaction,
not_null<QWidget*> context,
const style::WhoRead &st,
std::shared_ptr<WhoReadList> whoReadIds) {
const auto small = st.userpics.size;
const auto large = st.photoSize;
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto resolveWhoRead = reaction.isEmpty()
&& WhoReadExists(item);
const auto state = lifetime.make_state<State>();
const auto pushNext = [=] {
consumer.put_next_copy(state->current);
};
const auto resolveWhoReacted = !reaction.isEmpty()
|| item->canViewReactions();
auto idsWithReactions = (resolveWhoRead && resolveWhoReacted)
? WhoReadOrReactedIds(item, context)
: resolveWhoRead
? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions))
: WhoReactedIds(item, reaction, context);
state->current.type = resolveWhoRead
? DetectSeenType(item)
: Ui::WhoReadType::Reacted;
if (resolveWhoReacted) {
const auto &list = item->reactions();
state->current.fullReactionsCount = reaction.isEmpty()
? ranges::accumulate(
list,
0,
ranges::plus{},
[](const auto &pair) { return pair.second; })
: list.contains(reaction)
? list.find(reaction)->second
: 0;
// #TODO reactions
state->current.singleReaction = !reaction.isEmpty()
? reaction
: (list.size() == 1)
? list.front().first
: QString();
}
std::move(
idsWithReactions
) | rpl::start_with_next([=](PeersWithReactions &&peers) {
if (peers.unknown) {
state->userpics.clear();
consumer.put_next(Ui::WhoReadContent{
.type = state->current.type,
.fullReactionsCount = state->current.fullReactionsCount,
.fullReadCount = state->current.fullReadCount,
.unknown = true,
});
return;
}
state->current.fullReadCount = int(peers.read.size());
state->current.fullReactionsCount = peers.fullReactionsCount;
if (whoReadIds) {
whoReadIds->list = (peers.read.size() > peers.list.size())
? std::move(peers.read)
: std::vector<PeerId>();
}
if (UpdateUserpics(state, item, peers.list)) {
RegenerateParticipants(state, small, large);
pushNext();
} else if (peers.list.empty()) {
pushNext();
}
}, lifetime);
item->history()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return state->someUserpicsNotLoaded && !state->scheduled;
}) | rpl::start_with_next([=] {
for (const auto &userpic : state->userpics) {
if (userpic.peer->userpicUniqueKey(userpic.view)
!= userpic.uniqueKey) {
state->scheduled = true;
crl::on_main(&state->guard, [=] {
state->scheduled = false;
RegenerateUserpics(state, small, large);
pushNext();
});
return;
}
}
}, lifetime);
return lifetime;
};
}
} // namespace
bool WhoReadExists(not_null<HistoryItem*> item) {
@ -486,8 +585,9 @@ bool WhoReactedExists(not_null<HistoryItem*> item) {
rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
not_null<QWidget*> context,
const style::WhoRead &st) {
return WhoReacted(item, QString(), context, st);
const style::WhoRead &st,
std::shared_ptr<WhoReadList> whoReadIds) {
return WhoReacted(item, QString(), context, st, std::move(whoReadIds));
}
rpl::producer<Ui::WhoReadContent> WhoReacted(
@ -495,90 +595,7 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
const QString &reaction,
not_null<QWidget*> context,
const style::WhoRead &st) {
const auto small = st.userpics.size;
const auto large = st.photoSize;
return [=](auto consumer) {
auto lifetime = rpl::lifetime();
const auto resolveWhoRead = reaction.isEmpty() && WhoReadExists(item);
const auto state = lifetime.make_state<State>();
const auto pushNext = [=] {
consumer.put_next_copy(state->current);
};
const auto resolveWhoReacted = !reaction.isEmpty()
|| item->canViewReactions();
auto idsWithReactions = (resolveWhoRead && resolveWhoReacted)
? WhoReadOrReactedIds(item, context)
: resolveWhoRead
? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions))
: WhoReactedIds(item, reaction, context);
state->current.type = resolveWhoRead
? DetectSeenType(item)
: Ui::WhoReadType::Reacted;
if (resolveWhoReacted) {
const auto &list = item->reactions();
state->current.fullReactionsCount = reaction.isEmpty()
? ranges::accumulate(
list,
0,
ranges::plus{},
[](const auto &pair) { return pair.second; })
: list.contains(reaction)
? list.find(reaction)->second
: 0;
// #TODO reactions
state->current.singleReaction = !reaction.isEmpty()
? reaction
: (list.size() == 1)
? list.front().first
: QString();
}
std::move(
idsWithReactions
) | rpl::start_with_next([=](const PeersWithReactions &peers) {
if (peers.unknown) {
state->userpics.clear();
consumer.put_next(Ui::WhoReadContent{
.type = state->current.type,
.fullReactionsCount = state->current.fullReactionsCount,
.fullReadCount = state->current.fullReadCount,
.unknown = true,
});
return;
}
state->current.fullReadCount = peers.fullReadCount;
state->current.fullReactionsCount = peers.fullReactionsCount;
if (UpdateUserpics(state, item, peers.list)) {
RegenerateParticipants(state, small, large);
pushNext();
} else if (peers.list.empty()) {
pushNext();
}
}, lifetime);
item->history()->session().downloaderTaskFinished(
) | rpl::filter([=] {
return state->someUserpicsNotLoaded && !state->scheduled;
}) | rpl::start_with_next([=] {
for (const auto &userpic : state->userpics) {
if (userpic.peer->userpicUniqueKey(userpic.view)
!= userpic.uniqueKey) {
state->scheduled = true;
crl::on_main(&state->guard, [=] {
state->scheduled = false;
RegenerateUserpics(state, small, large);
pushNext();
});
return;
}
}
}, lifetime);
return lifetime;
};
return WhoReacted(item, reaction, context, st, nullptr);
}
} // namespace Api

View File

@ -15,6 +15,7 @@ struct WhoRead;
namespace Ui {
struct WhoReadContent;
enum class WhoReadType;
} // namespace Ui
namespace Api {
@ -22,15 +23,21 @@ namespace Api {
[[nodiscard]] bool WhoReadExists(not_null<HistoryItem*> item);
[[nodiscard]] bool WhoReactedExists(not_null<HistoryItem*> item);
struct WhoReadList {
std::vector<PeerId> list;
Ui::WhoReadType type = {};
};
// The context must be destroyed before the session holding this item.
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
not_null<QWidget*> context,
const style::WhoRead &st); // Cache results for this lifetime.
not_null<QWidget*> context, // Cache results for this lifetime.
const style::WhoRead &st,
std::shared_ptr<WhoReadList> whoReadIds = nullptr);
[[nodiscard]] rpl::producer<Ui::WhoReadContent> WhoReacted(
not_null<HistoryItem*> item,
const QString &reaction,
not_null<QWidget*> context,
const style::WhoRead &st); // Cache results for this lifetime.
not_null<QWidget*> context, // Cache results for this lifetime.
const style::WhoRead &st);
} // namespace Api

View File

@ -1078,6 +1078,7 @@ void AddWhoReactedAction(
not_null<QWidget*> context,
not_null<HistoryItem*> item,
not_null<Window::SessionController*> controller) {
const auto whoReadIds = std::make_shared<Api::WhoReadList>();
const auto participantChosen = [=](uint64 id) {
controller->showPeerInfo(PeerId(id));
};
@ -1091,7 +1092,8 @@ void AddWhoReactedAction(
controller->window().show(ReactionsListBox(
controller,
item,
QString()));
QString(),
whoReadIds));
}
};
if (!menu->empty()) {
@ -1099,7 +1101,7 @@ void AddWhoReactedAction(
}
menu->addAction(Ui::WhoReactedContextAction(
menu.get(),
Api::WhoReacted(item, context, st::defaultWhoRead),
Api::WhoReacted(item, context, st::defaultWhoRead, whoReadIds),
participantChosen,
showAllChosen));
}

View File

@ -13,6 +13,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_session_controller.h"
#include "history/history_item.h"
#include "history/history.h"
#include "api/api_who_reacted.h"
#include "ui/controls/who_reacted_context_action.h"
#include "main/main_session.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -50,7 +52,8 @@ public:
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
const QString &selected,
rpl::producer<QString> switches);
rpl::producer<QString> switches,
std::shared_ptr<Api::WhoReadList> whoReadIds);
Main::Session &session() const override;
void prepare() override;
@ -60,7 +63,8 @@ public:
private:
using AllEntry = std::pair<not_null<UserData*>, QString>;
void loadMore(const QString &offset);
void fillWhoRead();
void loadMore(const QString &reaction);
bool appendRow(not_null<UserData*> user, QString reaction);
std::unique_ptr<PeerListRow> createRow(
not_null<UserData*> user,
@ -72,6 +76,8 @@ private:
MTP::Sender _api;
QString _shownReaction;
std::shared_ptr<Api::WhoReadList> _whoReadIds;
std::vector<not_null<UserData*>> _whoRead;
std::vector<AllEntry> _all;
QString _allOffset;
@ -127,11 +133,13 @@ Controller::Controller(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
const QString &selected,
rpl::producer<QString> switches)
rpl::producer<QString> switches,
std::shared_ptr<Api::WhoReadList> whoReadIds)
: _window(window)
, _item(item)
, _api(&window->session().mtp())
, _shownReaction(selected) {
, _shownReaction(selected)
, _whoReadIds(whoReadIds) {
std::move(
switches
) | rpl::filter([=](const QString &reaction) {
@ -146,10 +154,14 @@ Main::Session &Controller::session() const {
}
void Controller::prepare() {
setDescriptionText(tr::lng_contacts_loading(tr::now));
if (_shownReaction == u"read"_q) {
fillWhoRead();
setDescriptionText(QString());
} else {
setDescriptionText(tr::lng_contacts_loading(tr::now));
}
delegate()->peerListRefreshRows();
loadMore(QString());
loadMore(_shownReaction);
}
void Controller::showReaction(const QString &reaction) {
@ -163,7 +175,9 @@ void Controller::showReaction(const QString &reaction) {
}
_shownReaction = reaction;
if (_shownReaction.isEmpty()) {
if (_shownReaction == u"read"_q) {
fillWhoRead();
} else if (_shownReaction.isEmpty()) {
_filtered.clear();
for (const auto &[user, reaction] : _all) {
appendRow(user, reaction);
@ -177,14 +191,29 @@ void Controller::showReaction(const QString &reaction) {
for (const auto user : _filtered) {
appendRow(user, _shownReaction);
}
loadMore(QString());
_filteredOffset = QString();
}
loadMore(_shownReaction);
setDescriptionText(delegate()->peerListFullRowsCount()
? QString()
: tr::lng_contacts_loading(tr::now));
delegate()->peerListRefreshRows();
}
void Controller::fillWhoRead() {
if (_whoReadIds && !_whoReadIds->list.empty() && _whoRead.empty()) {
auto &owner = _window->session().data();
for (const auto &peerId : _whoReadIds->list) {
if (const auto user = owner.userLoaded(peerToUser(peerId))) {
_whoRead.push_back(user);
}
}
}
for (const auto &user : _whoRead) {
appendRow(user, QString());
}
}
void Controller::loadMoreRows() {
const auto &offset = _shownReaction.isEmpty()
? _allOffset
@ -192,26 +221,37 @@ void Controller::loadMoreRows() {
if (_loadRequestId || offset.isEmpty()) {
return;
}
loadMore(offset);
loadMore(_shownReaction);
}
void Controller::loadMore(const QString &offset) {
void Controller::loadMore(const QString &reaction) {
if (reaction == u"read"_q) {
loadMore(QString());
return;
} else if (reaction.isEmpty() && _allOffset.isEmpty() && !_all.empty()) {
return;
}
_api.request(_loadRequestId).cancel();
const auto &offset = reaction.isEmpty()
? _allOffset
: _filteredOffset;
using Flag = MTPmessages_GetMessageReactionsList::Flag;
const auto flags = Flag(0)
| (offset.isEmpty() ? Flag(0) : Flag::f_offset)
| (_shownReaction.isEmpty() ? Flag(0) : Flag::f_reaction);
| (reaction.isEmpty() ? Flag(0) : Flag::f_reaction);
_loadRequestId = _api.request(MTPmessages_GetMessageReactionsList(
MTP_flags(flags),
_item->history()->peer->input,
MTP_int(_item->id),
MTP_string(_shownReaction),
MTP_string(reaction),
MTP_string(offset),
MTP_int(kPerPageFirst)
)).done([=](const MTPmessages_MessageReactionsList &result) {
_loadRequestId = 0;
const auto filtered = !_shownReaction.isEmpty();
const auto filtered = !reaction.isEmpty();
const auto shown = (reaction == _shownReaction);
result.match([&](const MTPDmessages_messageReactionsList &data) {
const auto sessionData = &session().data();
sessionData->processUsers(data.vusers());
@ -222,7 +262,7 @@ void Controller::loadMore(const QString &offset) {
const auto user = sessionData->userLoaded(
data.vuser_id().v);
const auto reaction = qs(data.vreaction());
if (user && appendRow(user, reaction)) {
if (user && (!shown || appendRow(user, reaction))) {
if (filtered) {
_filtered.emplace_back(user);
} else {
@ -232,8 +272,10 @@ void Controller::loadMore(const QString &offset) {
});
}
});
setDescriptionText(QString());
delegate()->peerListRefreshRows();
if (shown) {
setDescriptionText(QString());
delegate()->peerListRefreshRows();
}
}).send();
}
@ -264,20 +306,29 @@ std::unique_ptr<PeerListRow> Controller::createRow(
object_ptr<Ui::BoxContent> ReactionsListBox(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
QString selected) {
QString selected,
std::shared_ptr<Api::WhoReadList> whoReadIds) {
Expects(IsServerMsgId(item->id));
if (!item->reactions().contains(selected)) {
selected = QString();
}
if (selected.isEmpty() && whoReadIds && !whoReadIds->list.empty()) {
selected = u"read"_q;
}
const auto tabRequests = std::make_shared<rpl::event_stream<QString>>();
const auto initBox = [=](not_null<PeerListBox*> box) {
box->setNoContentMargin(true);
auto map = item->reactions();
if (whoReadIds && !whoReadIds->list.empty()) {
map.emplace(u"read"_q, int(whoReadIds->list.size()));
}
const auto selector = CreateReactionSelector(
box,
item->reactions(),
selected);
map,
selected,
whoReadIds ? whoReadIds->type : Ui::WhoReadType::Reacted);
selector->changes(
) | rpl::start_to_stream(*tabRequests, box->lifetime());
@ -299,7 +350,8 @@ object_ptr<Ui::BoxContent> ReactionsListBox(
window,
item,
selected,
tabRequests->events()),
tabRequests->events(),
whoReadIds),
initBox);
}

View File

@ -11,6 +11,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class HistoryItem;
namespace Api {
struct WhoReadList;
} // namespace Api
namespace Window {
class SessionController;
} // namespace Window
@ -24,6 +28,7 @@ namespace HistoryView {
object_ptr<Ui::BoxContent> ReactionsListBox(
not_null<Window::SessionController*> window,
not_null<HistoryItem*> item,
QString selected);
QString selected,
std::shared_ptr<Api::WhoReadList> whoReadIds = nullptr);
} // namespace HistoryView

View File

@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/abstract_button.h"
#include "ui/controls/who_reacted_context_action.h"
#include "styles/style_widgets.h"
#include "styles/style_chat.h"
@ -19,6 +20,7 @@ not_null<Ui::AbstractButton*> CreateTab(
not_null<QWidget*> parent,
const style::MultiSelect &st,
const QString &reaction,
Ui::WhoReadType whoReadType,
int count,
rpl::producer<bool> selected) {
struct State {
@ -71,9 +73,19 @@ not_null<Ui::AbstractButton*> CreateTab(
const auto shift = (height - (size / factor)) / 2;
Ui::Emoji::Draw(p, emoji, size, icon.x() + shift, shift);
} else {
(state->selected
? st::reactionsTabAllSelected
: st::reactionsTabAll).paintInCenter(p, icon);
using Type = Ui::WhoReadType;
(reaction.isEmpty()
? (state->selected
? st::reactionsTabAllSelected
: st::reactionsTabAll)
: (whoReadType == Type::Watched
|| whoReadType == Type::Listened)
? (state->selected
? st::reactionsTabPlayedSelected
: st::reactionsTabPlayed)
: (state->selected
? st::reactionsTabChecksSelected
: st::reactionsTabChecks)).paintInCenter(p, icon);
}
const auto textLeft = height + stm->padding.left();
@ -91,23 +103,14 @@ not_null<Ui::AbstractButton*> CreateTab(
not_null<Selector*> CreateReactionSelector(
not_null<QWidget*> parent,
const base::flat_map<QString, int> &items,
const QString &selected) {
const QString &selected,
Ui::WhoReadType whoReadType) {
struct State {
rpl::variable<QString> selected;
std::vector<not_null<Ui::AbstractButton*>> tabs;
};
const auto result = Ui::CreateChild<Selector>(parent.get());
using Entry = std::pair<int, QString>;
auto sorted = std::vector<Entry>();
for (const auto &[reaction, count] : items) {
sorted.emplace_back(count, reaction);
}
ranges::sort(sorted, std::greater<>(), &Entry::first);
const auto count = ranges::accumulate(
sorted,
0,
std::plus<>(),
&Entry::first);
auto tabs = Ui::CreateChild<Ui::RpWidget>(parent.get());
const auto st = &st::reactionsTabs;
const auto state = tabs->lifetime().make_state<State>();
@ -118,6 +121,7 @@ not_null<Selector*> CreateReactionSelector(
tabs,
*st,
reaction,
whoReadType,
count,
state->selected.value() | rpl::map(_1 == reaction));
tab->setClickedCallback([=] {
@ -125,6 +129,20 @@ not_null<Selector*> CreateReactionSelector(
});
state->tabs.push_back(tab);
};
auto sorted = std::vector<Entry>();
for (const auto &[reaction, count] : items) {
if (reaction == u"read"_q) {
append(reaction, count);
} else {
sorted.emplace_back(count, reaction);
}
}
ranges::sort(sorted, std::greater<>(), &Entry::first);
const auto count = ranges::accumulate(
sorted,
0,
std::plus<>(),
&Entry::first);
append(QString(), count);
for (const auto &[count, reaction] : sorted) {
append(reaction, count);

View File

@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Ui {
enum class WhoReadType;
} // namespace Ui
namespace HistoryView {
struct Selector {
@ -19,6 +23,7 @@ struct Selector {
not_null<Selector*> CreateReactionSelector(
not_null<QWidget*> parent,
const base::flat_map<QString, int> &items,
const QString &selected);
const QString &selected,
Ui::WhoReadType whoReadType);
} // namespace HistoryView

View File

@ -926,6 +926,10 @@ whoReadReactionsDisabled: icon{{ "menu/read_reactions", menuFgDisabled }};
reactionsTabAll: icon {{ "menu/read_reactions", windowFg }};
reactionsTabAllSelected: icon {{ "menu/read_reactions", activeButtonFg }};
reactionsTabPlayed: icon {{ "menu/read_audio", windowFg }};
reactionsTabPlayedSelected: icon {{ "menu/read_audio", activeButtonFg }};
reactionsTabChecks: icon {{ "menu/read_ticks", windowFg }};
reactionsTabChecksSelected: icon {{ "menu/read_ticks", activeButtonFg }};
reactionsTabs: MultiSelect(defaultMultiSelect) {
padding: margins(12px, 10px, 12px, 10px);
}

View File

@ -352,6 +352,10 @@ void Action::paint(Painter &p) {
void Action::refreshText() {
const auto usersCount = int(_content.participants.size());
const auto onlySeenCount = ranges::count(
_content.participants,
QString(),
&WhoReadParticipant::reaction);
const auto count = std::max(_content.fullReactionsCount, usersCount);
_text.setMarkedText(
_st.itemStyle,
@ -365,7 +369,8 @@ void Action::refreshText() {
_content.fullReactionsCount,
_content.fullReadCount)
: (_content.type == WhoReadType::Reacted
|| (count > 0 && _content.fullReactionsCount > usersCount))
|| (count > 0 && _content.fullReactionsCount > usersCount)
|| (count > 0 && onlySeenCount == 0))
? (count
? tr::lng_context_seen_reacted(
tr::now,