diff --git a/Telegram/Resources/icons/menu/read_ticks.png b/Telegram/Resources/icons/menu/read_ticks.png index 70b137342..af0709d54 100644 Binary files a/Telegram/Resources/icons/menu/read_ticks.png and b/Telegram/Resources/icons/menu/read_ticks.png differ diff --git a/Telegram/Resources/icons/menu/read_ticks@2x.png b/Telegram/Resources/icons/menu/read_ticks@2x.png index 244dde2ad..edea7820e 100644 Binary files a/Telegram/Resources/icons/menu/read_ticks@2x.png and b/Telegram/Resources/icons/menu/read_ticks@2x.png differ diff --git a/Telegram/Resources/icons/menu/read_ticks@3x.png b/Telegram/Resources/icons/menu/read_ticks@3x.png index 64fa1f2b4..1e90e9d28 100644 Binary files a/Telegram/Resources/icons/menu/read_ticks@3x.png and b/Telegram/Resources/icons/menu/read_ticks@3x.png differ diff --git a/Telegram/Resources/icons/menu/read_ticks_s.png b/Telegram/Resources/icons/menu/read_ticks_s.png new file mode 100644 index 000000000..7482108a6 Binary files /dev/null and b/Telegram/Resources/icons/menu/read_ticks_s.png differ diff --git a/Telegram/Resources/icons/menu/read_ticks_s@2x.png b/Telegram/Resources/icons/menu/read_ticks_s@2x.png new file mode 100644 index 000000000..2566c99cb Binary files /dev/null and b/Telegram/Resources/icons/menu/read_ticks_s@2x.png differ diff --git a/Telegram/Resources/icons/menu/read_ticks_s@3x.png b/Telegram/Resources/icons/menu/read_ticks_s@3x.png new file mode 100644 index 000000000..cf5d9d82b Binary files /dev/null and b/Telegram/Resources/icons/menu/read_ticks_s@3x.png differ diff --git a/Telegram/SourceFiles/api/api_who_reacted.cpp b/Telegram/SourceFiles/api/api_who_reacted.cpp index 121b75901..815f85d59 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.cpp +++ b/Telegram/SourceFiles/api/api_who_reacted.cpp @@ -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 list; + std::vector 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 list; - std::vector read; + std::vector 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 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, not_null 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, int small, int large) { } void RegenerateParticipants(not_null 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 WhoReacted( &PeerWithReaction::reaction); whoReadIds->list = (peers.read.size() > reacted) ? std::move(peers.read) - : std::vector(); + : std::vector(); } if (UpdateUserpics(state, item, peers.list)) { RegenerateParticipants(state, small, large); diff --git a/Telegram/SourceFiles/api/api_who_reacted.h b/Telegram/SourceFiles/api/api_who_reacted.h index b79b47584..b51623041 100644 --- a/Telegram/SourceFiles/api/api_who_reacted.h +++ b/Telegram/SourceFiles/api/api_who_reacted.h @@ -34,8 +34,17 @@ enum class WhoReactedList { not_null 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 list; + std::vector list; Ui::WhoReadType type = {}; }; diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp index 05769f543..f925dcb90 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_list.cpp @@ -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); } } diff --git a/Telegram/SourceFiles/lang/lang_keys.cpp b/Telegram/SourceFiles/lang/lang_keys.cpp index 51f581a04..aa65ed81d 100644 --- a/Telegram/SourceFiles/lang/lang_keys.cpp +++ b/Telegram/SourceFiles/lang/lang_keys.cpp @@ -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 diff --git a/Telegram/SourceFiles/lang/lang_keys.h b/Telegram/SourceFiles/lang/lang_keys.h index 87934fcce..f49f4fd32 100644 --- a/Telegram/SourceFiles/lang/lang_keys.h +++ b/Telegram/SourceFiles/lang/lang_keys.h @@ -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 diff --git a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp index 2f77253c1..a56957bc4 100644 --- a/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp +++ b/Telegram/SourceFiles/ui/boxes/choose_date_time.cpp @@ -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())); } diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index a83726419..d5768e364 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -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 }}; diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp index 98c261e1c..5b17e3832 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.cpp @@ -73,6 +73,7 @@ using Text::CustomEmojiFactory; struct EntryData { QString text; + QString date; QString customEntityData; QImage userpic; Fn 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 _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, diff --git a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h index 46d2cecf9..49d690ec4 100644 --- a/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h +++ b/Telegram/SourceFiles/ui/controls/who_reacted_context_action.h @@ -19,6 +19,7 @@ class PopupMenu; struct WhoReadParticipant { QString name; + QString date; QString customEntityData; QImage userpicSmall; QImage userpicLarge;