Implement dates of who read your message list.

This commit is contained in:
John Preston 2023-03-03 17:15:02 +04:00 committed by 23rd
parent af51307aa6
commit b95ea28e12
15 changed files with 214 additions and 122 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 471 B

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 720 B

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,008 B

View file

@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_media_types.h"
#include "data/data_message_reaction_id.h"
#include "lang/lang_keys.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "main/main_account.h"
@ -36,37 +37,33 @@ constexpr auto kContextReactionsLimit = 50;
using Data::ReactionId;
struct Peers {
std::vector<PeerId> list;
std::vector<WhoReadPeer> list;
bool unknown = false;
friend inline bool operator==(
const Peers &a,
const Peers &b) noexcept = default;
};
inline bool operator==(const Peers &a, const Peers &b) noexcept {
return (a.list == b.list) && (a.unknown == b.unknown);
}
struct PeerWithReaction {
PeerId peer = 0;
WhoReadPeer peerWithDate;
ReactionId reaction;
};
inline bool operator==(
friend inline bool operator==(
const PeerWithReaction &a,
const PeerWithReaction &b) noexcept {
return (a.peer == b.peer) && (a.reaction == b.reaction);
}
const PeerWithReaction &b) noexcept = default;
};
struct PeersWithReactions {
std::vector<PeerWithReaction> list;
std::vector<PeerId> read;
std::vector<WhoReadPeer> read;
int fullReactionsCount = 0;
bool unknown = false;
};
inline bool operator==(
friend inline bool operator==(
const PeersWithReactions &a,
const PeersWithReactions &b) noexcept {
return (a.fullReactionsCount == b.fullReactionsCount)
&& (a.list == b.list)
&& (a.read == b.read)
&& (a.unknown == b.unknown);
}
const PeersWithReactions &b) noexcept = default;
};
struct CachedRead {
CachedRead()
@ -113,6 +110,7 @@ struct Context {
struct Userpic {
not_null<PeerData*> peer;
TimeId date = 0;
QString customEntityData;
mutable Ui::PeerUserpicView view;
mutable InMemoryKey uniqueKey;
@ -234,7 +232,10 @@ struct State {
auto parsed = Peers();
parsed.list.reserve(result.v.size());
for (const auto &id : result.v) {
parsed.list.push_back(UserId(id.data().vuser_id()));
parsed.list.push_back({
.peer = UserId(id.data().vuser_id()),
.date = id.data().vdate().v,
});
}
entry.data = std::move(parsed);
}).fail([=] {
@ -252,8 +253,8 @@ struct State {
[[nodiscard]] PeersWithReactions WithEmptyReactions(
Peers &&peers) {
auto result = PeersWithReactions{
.list = peers.list | ranges::views::transform([](PeerId peer) {
return PeerWithReaction{ .peer = peer };
.list = peers.list | ranges::views::transform([](WhoReadPeer peer) {
return PeerWithReaction{ .peerWithDate = peer };
}) | ranges::to_vector,
.unknown = peers.unknown,
};
@ -302,7 +303,9 @@ struct State {
for (const auto &vote : data.vreactions().v) {
vote.match([&](const auto &data) {
parsed.list.push_back(PeerWithReaction{
.peer = peerFromMTP(data.vpeer_id()),
.peerWithDate = {
.peer = peerFromMTP(data.vpeer_id()),
},
.reaction = Data::ReactionFromMTP(
data.vreaction()),
});
@ -334,9 +337,16 @@ struct State {
return PeersWithReactions{ .unknown = true };
}
auto &list = reacted.list;
for (const auto &peer : read.list) {
if (!ranges::contains(list, peer, &PeerWithReaction::peer)) {
list.push_back({ .peer = peer });
for (const auto &peerWithDate : read.list) {
const auto i = ranges::find(
list,
peerWithDate.peer,
[](const PeerWithReaction &p) {
return p.peerWithDate.peer; });
if (i != end(list)) {
i->peerWithDate.date = peerWithDate.date;
} else {
list.push_back({ .peerWithDate = peerWithDate });
}
}
reacted.read = std::move(read.list);
@ -344,6 +354,37 @@ struct State {
});
}
[[nodiscard]] QString FormatReadDate(TimeId date, const QDateTime &now) {
if (!date) {
return {};
}
const auto parsed = base::unixtime::parse(date);
const auto readDate = parsed.date();
const auto nowDate = now.date();
if (readDate == nowDate) {
return tr::lng_mediaview_today(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
} else if (readDate.addDays(1) == nowDate) {
return tr::lng_mediaview_yesterday(
tr::now,
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
return tr::lng_mediaview_date_time(
tr::now,
lt_date,
tr::lng_month_day(
tr::now,
lt_month,
Lang::MonthDay(readDate.month())(tr::now),
lt_day,
QString::number(readDate.day())),
lt_time,
QLocale().toString(parsed.time(), QLocale::ShortFormat));
}
bool UpdateUserpics(
not_null<State*> state,
not_null<HistoryItem*> item,
@ -352,13 +393,15 @@ bool UpdateUserpics(
struct ResolvedPeer {
PeerData *peer = nullptr;
TimeId date = 0;
ReactionId reaction;
};
const auto peers = ranges::views::all(
ids
) | ranges::views::transform([&](PeerWithReaction id) {
return ResolvedPeer{
.peer = owner.peerLoaded(id.peer),
.peer = owner.peerLoaded(id.peerWithDate.peer),
.date = id.peerWithDate.date,
.reaction = id.reaction,
};
}) | ranges::views::filter([](ResolvedPeer resolved) {
@ -369,8 +412,8 @@ bool UpdateUserpics(
state->userpics,
peers,
ranges::equal_to(),
&Userpic::peer,
[](const ResolvedPeer &r) { return not_null{ r.peer }; });
[](const Userpic &u) { return std::pair(u.peer.get(), u.date); },
[](const ResolvedPeer &r) { return std::pair(r.peer, r.date); });
if (same) {
return false;
}
@ -381,12 +424,14 @@ bool UpdateUserpics(
const auto &data = ReactionEntityData(resolved.reaction);
const auto i = ranges::find(was, peer, &Userpic::peer);
if (i != end(was) && i->view.cloud) {
i->date = resolved.date;
now.push_back(std::move(*i));
now.back().customEntityData = data;
continue;
}
now.push_back(Userpic{
.peer = peer,
.date = resolved.date,
.customEntityData = data,
});
auto &userpic = now.back();
@ -422,20 +467,24 @@ void RegenerateUserpics(not_null<State*> state, int small, int large) {
}
void RegenerateParticipants(not_null<State*> state, int small, int large) {
const auto currentDate = QDateTime::currentDateTime();
auto old = base::take(state->current.participants);
auto &now = state->current.participants;
now.reserve(state->userpics.size());
for (auto &userpic : state->userpics) {
const auto peer = userpic.peer;
const auto date = userpic.date;
const auto id = peer->id.value;
const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
if (was != end(old)) {
was->name = peer->name();
was->date = FormatReadDate(date, currentDate);
now.push_back(std::move(*was));
continue;
}
now.push_back({
.name = peer->name(),
.date = FormatReadDate(date, currentDate),
.customEntityData = userpic.customEntityData,
.userpicLarge = GenerateUserpic(userpic, large),
.userpicKey = userpic.uniqueKey,
@ -522,7 +571,7 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
&PeerWithReaction::reaction);
whoReadIds->list = (peers.read.size() > reacted)
? std::move(peers.read)
: std::vector<PeerId>();
: std::vector<WhoReadPeer>();
}
if (UpdateUserpics(state, item, peers.list)) {
RegenerateParticipants(state, small, large);

View file

@ -34,8 +34,17 @@ enum class WhoReactedList {
not_null<HistoryItem*> item,
WhoReactedList list);
struct WhoReadPeer {
PeerId peer = 0;
TimeId date = 0;
friend inline bool operator==(
const WhoReadPeer &a,
const WhoReadPeer &b) noexcept = default;
};
struct WhoReadList {
std::vector<PeerId> list;
std::vector<WhoReadPeer> list;
Ui::WhoReadType type = {};
};

View file

@ -250,8 +250,8 @@ uint64 Controller::id(
void Controller::fillWhoRead() {
if (_whoReadIds && !_whoReadIds->list.empty() && _whoRead.empty()) {
auto &owner = _window->session().data();
for (const auto &peerId : _whoReadIds->list) {
if (const auto peer = owner.peerLoaded(peerId)) {
for (const auto &peerWithDate : _whoReadIds->list) {
if (const auto peer = owner.peerLoaded(peerWithDate.peer)) {
_whoRead.push_back(peer);
}
}

View file

@ -52,72 +52,7 @@ inline QString langDateMaybeWithYear(
return withoutYear(month, year);
}
tr::phrase<> Month(int index) {
switch (index) {
case 1: return tr::lng_month1;
case 2: return tr::lng_month2;
case 3: return tr::lng_month3;
case 4: return tr::lng_month4;
case 5: return tr::lng_month5;
case 6: return tr::lng_month6;
case 7: return tr::lng_month7;
case 8: return tr::lng_month8;
case 9: return tr::lng_month9;
case 10: return tr::lng_month10;
case 11: return tr::lng_month11;
case 12: return tr::lng_month12;
}
Unexpected("Index in MonthSmall.");
}
tr::phrase<> MonthSmall(int index) {
switch (index) {
case 1: return tr::lng_month1_small;
case 2: return tr::lng_month2_small;
case 3: return tr::lng_month3_small;
case 4: return tr::lng_month4_small;
case 5: return tr::lng_month5_small;
case 6: return tr::lng_month6_small;
case 7: return tr::lng_month7_small;
case 8: return tr::lng_month8_small;
case 9: return tr::lng_month9_small;
case 10: return tr::lng_month10_small;
case 11: return tr::lng_month11_small;
case 12: return tr::lng_month12_small;
}
Unexpected("Index in MonthSmall.");
}
tr::phrase<> MonthDay(int index) {
switch (index) {
case 1: return tr::lng_month_day1;
case 2: return tr::lng_month_day2;
case 3: return tr::lng_month_day3;
case 4: return tr::lng_month_day4;
case 5: return tr::lng_month_day5;
case 6: return tr::lng_month_day6;
case 7: return tr::lng_month_day7;
case 8: return tr::lng_month_day8;
case 9: return tr::lng_month_day9;
case 10: return tr::lng_month_day10;
case 11: return tr::lng_month_day11;
case 12: return tr::lng_month_day12;
}
Unexpected("Index in MonthDay.");
}
tr::phrase<> Weekday(int index) {
switch (index) {
case 1: return tr::lng_weekday1;
case 2: return tr::lng_weekday2;
case 3: return tr::lng_weekday3;
case 4: return tr::lng_weekday4;
case 5: return tr::lng_weekday5;
case 6: return tr::lng_weekday6;
case 7: return tr::lng_weekday7;
}
Unexpected("Index in Weekday.");
}
using namespace Lang;
} // namespace
@ -245,4 +180,71 @@ QString LanguageIdOrDefault(const QString &id) {
return !id.isEmpty() ? id : DefaultLanguageId();
}
tr::phrase<> Month(int index) {
switch (index) {
case 1: return tr::lng_month1;
case 2: return tr::lng_month2;
case 3: return tr::lng_month3;
case 4: return tr::lng_month4;
case 5: return tr::lng_month5;
case 6: return tr::lng_month6;
case 7: return tr::lng_month7;
case 8: return tr::lng_month8;
case 9: return tr::lng_month9;
case 10: return tr::lng_month10;
case 11: return tr::lng_month11;
case 12: return tr::lng_month12;
}
Unexpected("Index in MonthSmall.");
}
tr::phrase<> MonthSmall(int index) {
switch (index) {
case 1: return tr::lng_month1_small;
case 2: return tr::lng_month2_small;
case 3: return tr::lng_month3_small;
case 4: return tr::lng_month4_small;
case 5: return tr::lng_month5_small;
case 6: return tr::lng_month6_small;
case 7: return tr::lng_month7_small;
case 8: return tr::lng_month8_small;
case 9: return tr::lng_month9_small;
case 10: return tr::lng_month10_small;
case 11: return tr::lng_month11_small;
case 12: return tr::lng_month12_small;
}
Unexpected("Index in MonthSmall.");
}
tr::phrase<> MonthDay(int index) {
switch (index) {
case 1: return tr::lng_month_day1;
case 2: return tr::lng_month_day2;
case 3: return tr::lng_month_day3;
case 4: return tr::lng_month_day4;
case 5: return tr::lng_month_day5;
case 6: return tr::lng_month_day6;
case 7: return tr::lng_month_day7;
case 8: return tr::lng_month_day8;
case 9: return tr::lng_month_day9;
case 10: return tr::lng_month_day10;
case 11: return tr::lng_month_day11;
case 12: return tr::lng_month_day12;
}
Unexpected("Index in MonthDay.");
}
tr::phrase<> Weekday(int index) {
switch (index) {
case 1: return tr::lng_weekday1;
case 2: return tr::lng_weekday2;
case 3: return tr::lng_weekday3;
case 4: return tr::lng_weekday4;
case 5: return tr::lng_weekday5;
case 6: return tr::lng_weekday6;
case 7: return tr::lng_weekday7;
}
Unexpected("Index in Weekday.");
}
} // namespace Lang

View file

@ -37,4 +37,9 @@ namespace Lang {
[[nodiscard]] QString DefaultLanguageId();
[[nodiscard]] QString LanguageIdOrDefault(const QString &id);
[[nodiscard]] tr::phrase<> Month(int index);
[[nodiscard]] tr::phrase<> MonthSmall(int index);
[[nodiscard]] tr::phrase<> MonthDay(int index);
[[nodiscard]] tr::phrase<> Weekday(int index);
} // namespace Lang

View file

@ -23,29 +23,11 @@ namespace {
constexpr auto kMinimalSchedule = TimeId(10);
tr::phrase<> MonthDay(int index) {
switch (index) {
case 1: return tr::lng_month_day1;
case 2: return tr::lng_month_day2;
case 3: return tr::lng_month_day3;
case 4: return tr::lng_month_day4;
case 5: return tr::lng_month_day5;
case 6: return tr::lng_month_day6;
case 7: return tr::lng_month_day7;
case 8: return tr::lng_month_day8;
case 9: return tr::lng_month_day9;
case 10: return tr::lng_month_day10;
case 11: return tr::lng_month_day11;
case 12: return tr::lng_month_day12;
}
Unexpected("Index in MonthDay.");
}
QString DayString(const QDate &date) {
return tr::lng_month_day(
tr::now,
lt_month,
MonthDay(date.month())(tr::now),
Lang::MonthDay(date.month())(tr::now),
lt_day,
QString::number(date.day()));
}

View file

@ -1095,6 +1095,17 @@ whoReadMenu: PopupMenu(popupMenuExpandedSeparator) {
scrollPadding: margins(0px, 6px, 0px, 4px);
maxHeight: 400px;
}
whoReadNameWithDateTop: 3px;
whoReadDateTop: 20px;
whoReadDateSkip: 15px;
whoReadDateChecks: icon{{ "menu/read_ticks_s", windowSubTextFg }};
whoReadDateChecksOver: icon{{ "menu/read_ticks_s", windowSubTextFgOver }};
whoReadDateChecksPosition: point(-7px, -4px);
whoReadDateStyle: TextStyle(defaultTextStyle) {
font: font(12px);
linkFont: font(12px);
linkFontOver: font(12px underline);
}
whoReadChecks: icon{{ "menu/read_ticks", windowBoldFg }};
whoReadChecksOver: icon{{ "menu/read_ticks", windowBoldFg }};
whoReadChecksDisabled: icon{{ "menu/read_ticks", menuFgDisabled }};

View file

@ -73,6 +73,7 @@ using Text::CustomEmojiFactory;
struct EntryData {
QString text;
QString date;
QString customEntityData;
QImage userpic;
Fn<void()> callback;
@ -287,7 +288,7 @@ void Action::updateUserpicsFromContent() {
}
void Action::populateSubmenu() {
if (_content.participants.size() < 2) {
if (_content.participants.size() < 1) {
_submenu.clear();
_parentMenu->removeSubmenu(action());
if (!isEnabled()) {
@ -487,6 +488,7 @@ private:
const int _height = 0;
Text::String _text;
Text::String _date;
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
QImage _userpic;
int _textWidth = 0;
@ -533,11 +535,21 @@ void WhoReactedListMenu::EntryAction::setData(EntryData &&data) {
setClickedCallback(std::move(data.callback));
_userpic = std::move(data.userpic);
_text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);
if (data.date.isEmpty()) {
_date = Text::String();
} else {
_date.setMarkedText(
st::whoReadDateStyle,
{ data.date },
MenuTextOptions);
}
_custom = _customEmojiFactory(data.customEntityData, [=] { update(); });
const auto ratio = style::DevicePixelRatio();
const auto size = Emoji::GetSizeNormal() / ratio;
_customSize = Text::AdjustCustomEmojiSize(size);
const auto textWidth = _text.maxWidth();
const auto textWidth = std::max(
_text.maxWidth(),
st::whoReadDateSkip + _date.maxWidth());
const auto &padding = _st.itemPadding;
const auto rightSkip = padding.right()
+ (_custom ? (size + padding.right()) : 0);
@ -571,6 +583,10 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
QRect(photoLeft, photoTop, photoSize, photoSize));
}
const auto withDate = !_date.isEmpty();
const auto textTop = withDate
? st::whoReadNameWithDateTop
: (height() - _st.itemStyle.font->height) / 2;
p.setPen(selected
? _st.itemFgOver
: enabled
@ -579,10 +595,25 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
_text.drawLeftElided(
p,
st::defaultWhoRead.nameLeft,
(height() - _st.itemStyle.font->height) / 2,
textTop,
_textWidth,
width());
if (withDate) {
const auto iconPosition = QPoint(
st::defaultWhoRead.nameLeft,
st::whoReadDateTop) + st::whoReadDateChecksPosition;
const auto &icon = selected
? st::whoReadDateChecksOver
: st::whoReadDateChecks;
icon.paint(p, iconPosition, width());
p.setPen(selected ? _st.itemFgShortcutOver : _st.itemFgShortcut);
_date.drawLeftElided(
p,
st::defaultWhoRead.nameLeft + st::whoReadDateSkip,
st::whoReadDateTop,
_textWidth - st::whoReadDateSkip,
width());
}
if (_custom) {
const auto ratio = style::DevicePixelRatio();
const auto size = Emoji::GetSizeNormal() / ratio;
@ -600,6 +631,7 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {
return (a.id == b.id)
&& (a.name == b.name)
&& (a.date == b.date)
&& (a.userpicKey == b.userpicKey);
}
@ -680,6 +712,7 @@ void WhoReactedListMenu::populate(
};
append({
.text = participant.name,
.date = participant.date,
.customEntityData = participant.customEntityData,
.userpic = participant.userpicLarge,
.callback = chosen,

View file

@ -19,6 +19,7 @@ class PopupMenu;
struct WhoReadParticipant {
QString name;
QString date;
QString customEntityData;
QImage userpicSmall;
QImage userpicLarge;