Implement dates of who read your message list.
Before Width: | Height: | Size: 471 B After Width: | Height: | Size: 414 B |
Before Width: | Height: | Size: 720 B After Width: | Height: | Size: 812 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
Telegram/Resources/icons/menu/read_ticks_s.png
Normal file
After Width: | Height: | Size: 348 B |
BIN
Telegram/Resources/icons/menu/read_ticks_s@2x.png
Normal file
After Width: | Height: | Size: 695 B |
BIN
Telegram/Resources/icons/menu/read_ticks_s@3x.png
Normal file
After Width: | Height: | Size: 1,008 B |
|
@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "data/data_media_types.h"
|
#include "data/data_media_types.h"
|
||||||
#include "data/data_message_reaction_id.h"
|
#include "data/data_message_reaction_id.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_app_config.h"
|
#include "main/main_app_config.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "main/main_account.h"
|
#include "main/main_account.h"
|
||||||
|
@ -36,37 +37,33 @@ constexpr auto kContextReactionsLimit = 50;
|
||||||
using Data::ReactionId;
|
using Data::ReactionId;
|
||||||
|
|
||||||
struct Peers {
|
struct Peers {
|
||||||
std::vector<PeerId> list;
|
std::vector<WhoReadPeer> list;
|
||||||
bool unknown = false;
|
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 {
|
struct PeerWithReaction {
|
||||||
PeerId peer = 0;
|
WhoReadPeer peerWithDate;
|
||||||
ReactionId reaction;
|
ReactionId reaction;
|
||||||
};
|
|
||||||
inline bool operator==(
|
friend inline bool operator==(
|
||||||
const PeerWithReaction &a,
|
const PeerWithReaction &a,
|
||||||
const PeerWithReaction &b) noexcept {
|
const PeerWithReaction &b) noexcept = default;
|
||||||
return (a.peer == b.peer) && (a.reaction == b.reaction);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
struct PeersWithReactions {
|
struct PeersWithReactions {
|
||||||
std::vector<PeerWithReaction> list;
|
std::vector<PeerWithReaction> list;
|
||||||
std::vector<PeerId> read;
|
std::vector<WhoReadPeer> read;
|
||||||
int fullReactionsCount = 0;
|
int fullReactionsCount = 0;
|
||||||
bool unknown = false;
|
bool unknown = false;
|
||||||
};
|
|
||||||
inline bool operator==(
|
friend inline bool operator==(
|
||||||
const PeersWithReactions &a,
|
const PeersWithReactions &a,
|
||||||
const PeersWithReactions &b) noexcept {
|
const PeersWithReactions &b) noexcept = default;
|
||||||
return (a.fullReactionsCount == b.fullReactionsCount)
|
};
|
||||||
&& (a.list == b.list)
|
|
||||||
&& (a.read == b.read)
|
|
||||||
&& (a.unknown == b.unknown);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CachedRead {
|
struct CachedRead {
|
||||||
CachedRead()
|
CachedRead()
|
||||||
|
@ -113,6 +110,7 @@ struct Context {
|
||||||
|
|
||||||
struct Userpic {
|
struct Userpic {
|
||||||
not_null<PeerData*> peer;
|
not_null<PeerData*> peer;
|
||||||
|
TimeId date = 0;
|
||||||
QString customEntityData;
|
QString customEntityData;
|
||||||
mutable Ui::PeerUserpicView view;
|
mutable Ui::PeerUserpicView view;
|
||||||
mutable InMemoryKey uniqueKey;
|
mutable InMemoryKey uniqueKey;
|
||||||
|
@ -234,7 +232,10 @@ struct State {
|
||||||
auto parsed = Peers();
|
auto parsed = Peers();
|
||||||
parsed.list.reserve(result.v.size());
|
parsed.list.reserve(result.v.size());
|
||||||
for (const auto &id : result.v) {
|
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);
|
entry.data = std::move(parsed);
|
||||||
}).fail([=] {
|
}).fail([=] {
|
||||||
|
@ -252,8 +253,8 @@ struct State {
|
||||||
[[nodiscard]] PeersWithReactions WithEmptyReactions(
|
[[nodiscard]] PeersWithReactions WithEmptyReactions(
|
||||||
Peers &&peers) {
|
Peers &&peers) {
|
||||||
auto result = PeersWithReactions{
|
auto result = PeersWithReactions{
|
||||||
.list = peers.list | ranges::views::transform([](PeerId peer) {
|
.list = peers.list | ranges::views::transform([](WhoReadPeer peer) {
|
||||||
return PeerWithReaction{ .peer = peer };
|
return PeerWithReaction{ .peerWithDate = peer };
|
||||||
}) | ranges::to_vector,
|
}) | ranges::to_vector,
|
||||||
.unknown = peers.unknown,
|
.unknown = peers.unknown,
|
||||||
};
|
};
|
||||||
|
@ -302,7 +303,9 @@ struct State {
|
||||||
for (const auto &vote : data.vreactions().v) {
|
for (const auto &vote : data.vreactions().v) {
|
||||||
vote.match([&](const auto &data) {
|
vote.match([&](const auto &data) {
|
||||||
parsed.list.push_back(PeerWithReaction{
|
parsed.list.push_back(PeerWithReaction{
|
||||||
.peer = peerFromMTP(data.vpeer_id()),
|
.peerWithDate = {
|
||||||
|
.peer = peerFromMTP(data.vpeer_id()),
|
||||||
|
},
|
||||||
.reaction = Data::ReactionFromMTP(
|
.reaction = Data::ReactionFromMTP(
|
||||||
data.vreaction()),
|
data.vreaction()),
|
||||||
});
|
});
|
||||||
|
@ -334,9 +337,16 @@ struct State {
|
||||||
return PeersWithReactions{ .unknown = true };
|
return PeersWithReactions{ .unknown = true };
|
||||||
}
|
}
|
||||||
auto &list = reacted.list;
|
auto &list = reacted.list;
|
||||||
for (const auto &peer : read.list) {
|
for (const auto &peerWithDate : read.list) {
|
||||||
if (!ranges::contains(list, peer, &PeerWithReaction::peer)) {
|
const auto i = ranges::find(
|
||||||
list.push_back({ .peer = peer });
|
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);
|
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(
|
bool UpdateUserpics(
|
||||||
not_null<State*> state,
|
not_null<State*> state,
|
||||||
not_null<HistoryItem*> item,
|
not_null<HistoryItem*> item,
|
||||||
|
@ -352,13 +393,15 @@ bool UpdateUserpics(
|
||||||
|
|
||||||
struct ResolvedPeer {
|
struct ResolvedPeer {
|
||||||
PeerData *peer = nullptr;
|
PeerData *peer = nullptr;
|
||||||
|
TimeId date = 0;
|
||||||
ReactionId reaction;
|
ReactionId reaction;
|
||||||
};
|
};
|
||||||
const auto peers = ranges::views::all(
|
const auto peers = ranges::views::all(
|
||||||
ids
|
ids
|
||||||
) | ranges::views::transform([&](PeerWithReaction id) {
|
) | ranges::views::transform([&](PeerWithReaction id) {
|
||||||
return ResolvedPeer{
|
return ResolvedPeer{
|
||||||
.peer = owner.peerLoaded(id.peer),
|
.peer = owner.peerLoaded(id.peerWithDate.peer),
|
||||||
|
.date = id.peerWithDate.date,
|
||||||
.reaction = id.reaction,
|
.reaction = id.reaction,
|
||||||
};
|
};
|
||||||
}) | ranges::views::filter([](ResolvedPeer resolved) {
|
}) | ranges::views::filter([](ResolvedPeer resolved) {
|
||||||
|
@ -369,8 +412,8 @@ bool UpdateUserpics(
|
||||||
state->userpics,
|
state->userpics,
|
||||||
peers,
|
peers,
|
||||||
ranges::equal_to(),
|
ranges::equal_to(),
|
||||||
&Userpic::peer,
|
[](const Userpic &u) { return std::pair(u.peer.get(), u.date); },
|
||||||
[](const ResolvedPeer &r) { return not_null{ r.peer }; });
|
[](const ResolvedPeer &r) { return std::pair(r.peer, r.date); });
|
||||||
if (same) {
|
if (same) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -381,12 +424,14 @@ bool UpdateUserpics(
|
||||||
const auto &data = ReactionEntityData(resolved.reaction);
|
const auto &data = ReactionEntityData(resolved.reaction);
|
||||||
const auto i = ranges::find(was, peer, &Userpic::peer);
|
const auto i = ranges::find(was, peer, &Userpic::peer);
|
||||||
if (i != end(was) && i->view.cloud) {
|
if (i != end(was) && i->view.cloud) {
|
||||||
|
i->date = resolved.date;
|
||||||
now.push_back(std::move(*i));
|
now.push_back(std::move(*i));
|
||||||
now.back().customEntityData = data;
|
now.back().customEntityData = data;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
now.push_back(Userpic{
|
now.push_back(Userpic{
|
||||||
.peer = peer,
|
.peer = peer,
|
||||||
|
.date = resolved.date,
|
||||||
.customEntityData = data,
|
.customEntityData = data,
|
||||||
});
|
});
|
||||||
auto &userpic = now.back();
|
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) {
|
void RegenerateParticipants(not_null<State*> state, int small, int large) {
|
||||||
|
const auto currentDate = QDateTime::currentDateTime();
|
||||||
auto old = base::take(state->current.participants);
|
auto old = base::take(state->current.participants);
|
||||||
auto &now = state->current.participants;
|
auto &now = state->current.participants;
|
||||||
now.reserve(state->userpics.size());
|
now.reserve(state->userpics.size());
|
||||||
for (auto &userpic : state->userpics) {
|
for (auto &userpic : state->userpics) {
|
||||||
const auto peer = userpic.peer;
|
const auto peer = userpic.peer;
|
||||||
|
const auto date = userpic.date;
|
||||||
const auto id = peer->id.value;
|
const auto id = peer->id.value;
|
||||||
const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
|
const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
|
||||||
if (was != end(old)) {
|
if (was != end(old)) {
|
||||||
was->name = peer->name();
|
was->name = peer->name();
|
||||||
|
was->date = FormatReadDate(date, currentDate);
|
||||||
now.push_back(std::move(*was));
|
now.push_back(std::move(*was));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
now.push_back({
|
now.push_back({
|
||||||
.name = peer->name(),
|
.name = peer->name(),
|
||||||
|
.date = FormatReadDate(date, currentDate),
|
||||||
.customEntityData = userpic.customEntityData,
|
.customEntityData = userpic.customEntityData,
|
||||||
.userpicLarge = GenerateUserpic(userpic, large),
|
.userpicLarge = GenerateUserpic(userpic, large),
|
||||||
.userpicKey = userpic.uniqueKey,
|
.userpicKey = userpic.uniqueKey,
|
||||||
|
@ -522,7 +571,7 @@ rpl::producer<Ui::WhoReadContent> WhoReacted(
|
||||||
&PeerWithReaction::reaction);
|
&PeerWithReaction::reaction);
|
||||||
whoReadIds->list = (peers.read.size() > reacted)
|
whoReadIds->list = (peers.read.size() > reacted)
|
||||||
? std::move(peers.read)
|
? std::move(peers.read)
|
||||||
: std::vector<PeerId>();
|
: std::vector<WhoReadPeer>();
|
||||||
}
|
}
|
||||||
if (UpdateUserpics(state, item, peers.list)) {
|
if (UpdateUserpics(state, item, peers.list)) {
|
||||||
RegenerateParticipants(state, small, large);
|
RegenerateParticipants(state, small, large);
|
||||||
|
|
|
@ -34,8 +34,17 @@ enum class WhoReactedList {
|
||||||
not_null<HistoryItem*> item,
|
not_null<HistoryItem*> item,
|
||||||
WhoReactedList list);
|
WhoReactedList list);
|
||||||
|
|
||||||
|
struct WhoReadPeer {
|
||||||
|
PeerId peer = 0;
|
||||||
|
TimeId date = 0;
|
||||||
|
|
||||||
|
friend inline bool operator==(
|
||||||
|
const WhoReadPeer &a,
|
||||||
|
const WhoReadPeer &b) noexcept = default;
|
||||||
|
};
|
||||||
|
|
||||||
struct WhoReadList {
|
struct WhoReadList {
|
||||||
std::vector<PeerId> list;
|
std::vector<WhoReadPeer> list;
|
||||||
Ui::WhoReadType type = {};
|
Ui::WhoReadType type = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -250,8 +250,8 @@ uint64 Controller::id(
|
||||||
void Controller::fillWhoRead() {
|
void Controller::fillWhoRead() {
|
||||||
if (_whoReadIds && !_whoReadIds->list.empty() && _whoRead.empty()) {
|
if (_whoReadIds && !_whoReadIds->list.empty() && _whoRead.empty()) {
|
||||||
auto &owner = _window->session().data();
|
auto &owner = _window->session().data();
|
||||||
for (const auto &peerId : _whoReadIds->list) {
|
for (const auto &peerWithDate : _whoReadIds->list) {
|
||||||
if (const auto peer = owner.peerLoaded(peerId)) {
|
if (const auto peer = owner.peerLoaded(peerWithDate.peer)) {
|
||||||
_whoRead.push_back(peer);
|
_whoRead.push_back(peer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,72 +52,7 @@ inline QString langDateMaybeWithYear(
|
||||||
return withoutYear(month, year);
|
return withoutYear(month, year);
|
||||||
}
|
}
|
||||||
|
|
||||||
tr::phrase<> Month(int index) {
|
using namespace Lang;
|
||||||
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
|
} // namespace
|
||||||
|
|
||||||
|
@ -245,4 +180,71 @@ QString LanguageIdOrDefault(const QString &id) {
|
||||||
return !id.isEmpty() ? id : DefaultLanguageId();
|
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
|
} // namespace Lang
|
||||||
|
|
|
@ -37,4 +37,9 @@ namespace Lang {
|
||||||
[[nodiscard]] QString DefaultLanguageId();
|
[[nodiscard]] QString DefaultLanguageId();
|
||||||
[[nodiscard]] QString LanguageIdOrDefault(const QString &id);
|
[[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
|
} // namespace Lang
|
||||||
|
|
|
@ -23,29 +23,11 @@ namespace {
|
||||||
|
|
||||||
constexpr auto kMinimalSchedule = TimeId(10);
|
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) {
|
QString DayString(const QDate &date) {
|
||||||
return tr::lng_month_day(
|
return tr::lng_month_day(
|
||||||
tr::now,
|
tr::now,
|
||||||
lt_month,
|
lt_month,
|
||||||
MonthDay(date.month())(tr::now),
|
Lang::MonthDay(date.month())(tr::now),
|
||||||
lt_day,
|
lt_day,
|
||||||
QString::number(date.day()));
|
QString::number(date.day()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1095,6 +1095,17 @@ whoReadMenu: PopupMenu(popupMenuExpandedSeparator) {
|
||||||
scrollPadding: margins(0px, 6px, 0px, 4px);
|
scrollPadding: margins(0px, 6px, 0px, 4px);
|
||||||
maxHeight: 400px;
|
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 }};
|
whoReadChecks: icon{{ "menu/read_ticks", windowBoldFg }};
|
||||||
whoReadChecksOver: icon{{ "menu/read_ticks", windowBoldFg }};
|
whoReadChecksOver: icon{{ "menu/read_ticks", windowBoldFg }};
|
||||||
whoReadChecksDisabled: icon{{ "menu/read_ticks", menuFgDisabled }};
|
whoReadChecksDisabled: icon{{ "menu/read_ticks", menuFgDisabled }};
|
||||||
|
|
|
@ -73,6 +73,7 @@ using Text::CustomEmojiFactory;
|
||||||
|
|
||||||
struct EntryData {
|
struct EntryData {
|
||||||
QString text;
|
QString text;
|
||||||
|
QString date;
|
||||||
QString customEntityData;
|
QString customEntityData;
|
||||||
QImage userpic;
|
QImage userpic;
|
||||||
Fn<void()> callback;
|
Fn<void()> callback;
|
||||||
|
@ -287,7 +288,7 @@ void Action::updateUserpicsFromContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Action::populateSubmenu() {
|
void Action::populateSubmenu() {
|
||||||
if (_content.participants.size() < 2) {
|
if (_content.participants.size() < 1) {
|
||||||
_submenu.clear();
|
_submenu.clear();
|
||||||
_parentMenu->removeSubmenu(action());
|
_parentMenu->removeSubmenu(action());
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
|
@ -487,6 +488,7 @@ private:
|
||||||
const int _height = 0;
|
const int _height = 0;
|
||||||
|
|
||||||
Text::String _text;
|
Text::String _text;
|
||||||
|
Text::String _date;
|
||||||
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
|
std::unique_ptr<Ui::Text::CustomEmoji> _custom;
|
||||||
QImage _userpic;
|
QImage _userpic;
|
||||||
int _textWidth = 0;
|
int _textWidth = 0;
|
||||||
|
@ -533,11 +535,21 @@ void WhoReactedListMenu::EntryAction::setData(EntryData &&data) {
|
||||||
setClickedCallback(std::move(data.callback));
|
setClickedCallback(std::move(data.callback));
|
||||||
_userpic = std::move(data.userpic);
|
_userpic = std::move(data.userpic);
|
||||||
_text.setMarkedText(_st.itemStyle, { data.text }, MenuTextOptions);
|
_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(); });
|
_custom = _customEmojiFactory(data.customEntityData, [=] { update(); });
|
||||||
const auto ratio = style::DevicePixelRatio();
|
const auto ratio = style::DevicePixelRatio();
|
||||||
const auto size = Emoji::GetSizeNormal() / ratio;
|
const auto size = Emoji::GetSizeNormal() / ratio;
|
||||||
_customSize = Text::AdjustCustomEmojiSize(size);
|
_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 &padding = _st.itemPadding;
|
||||||
const auto rightSkip = padding.right()
|
const auto rightSkip = padding.right()
|
||||||
+ (_custom ? (size + padding.right()) : 0);
|
+ (_custom ? (size + padding.right()) : 0);
|
||||||
|
@ -571,6 +583,10 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
|
||||||
QRect(photoLeft, photoTop, photoSize, photoSize));
|
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
|
p.setPen(selected
|
||||||
? _st.itemFgOver
|
? _st.itemFgOver
|
||||||
: enabled
|
: enabled
|
||||||
|
@ -579,10 +595,25 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
|
||||||
_text.drawLeftElided(
|
_text.drawLeftElided(
|
||||||
p,
|
p,
|
||||||
st::defaultWhoRead.nameLeft,
|
st::defaultWhoRead.nameLeft,
|
||||||
(height() - _st.itemStyle.font->height) / 2,
|
textTop,
|
||||||
_textWidth,
|
_textWidth,
|
||||||
width());
|
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) {
|
if (_custom) {
|
||||||
const auto ratio = style::DevicePixelRatio();
|
const auto ratio = style::DevicePixelRatio();
|
||||||
const auto size = Emoji::GetSizeNormal() / ratio;
|
const auto size = Emoji::GetSizeNormal() / ratio;
|
||||||
|
@ -600,6 +631,7 @@ void WhoReactedListMenu::EntryAction::paint(Painter &&p) {
|
||||||
bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {
|
bool operator==(const WhoReadParticipant &a, const WhoReadParticipant &b) {
|
||||||
return (a.id == b.id)
|
return (a.id == b.id)
|
||||||
&& (a.name == b.name)
|
&& (a.name == b.name)
|
||||||
|
&& (a.date == b.date)
|
||||||
&& (a.userpicKey == b.userpicKey);
|
&& (a.userpicKey == b.userpicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -680,6 +712,7 @@ void WhoReactedListMenu::populate(
|
||||||
};
|
};
|
||||||
append({
|
append({
|
||||||
.text = participant.name,
|
.text = participant.name,
|
||||||
|
.date = participant.date,
|
||||||
.customEntityData = participant.customEntityData,
|
.customEntityData = participant.customEntityData,
|
||||||
.userpic = participant.userpicLarge,
|
.userpic = participant.userpicLarge,
|
||||||
.callback = chosen,
|
.callback = chosen,
|
||||||
|
|
|
@ -19,6 +19,7 @@ class PopupMenu;
|
||||||
|
|
||||||
struct WhoReadParticipant {
|
struct WhoReadParticipant {
|
||||||
QString name;
|
QString name;
|
||||||
|
QString date;
|
||||||
QString customEntityData;
|
QString customEntityData;
|
||||||
QImage userpicSmall;
|
QImage userpicSmall;
|
||||||
QImage userpicLarge;
|
QImage userpicLarge;
|
||||||
|
|