api_who_reacted.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "api/api_who_reacted.h"
  8. #include "api/api_global_privacy.h"
  9. #include "history/history_item.h"
  10. #include "history/history.h"
  11. #include "data/stickers/data_custom_emoji.h"
  12. #include "data/data_peer.h"
  13. #include "data/data_chat.h"
  14. #include "data/data_channel.h"
  15. #include "data/data_document.h"
  16. #include "data/data_user.h"
  17. #include "data/data_changes.h"
  18. #include "data/data_session.h"
  19. #include "data/data_media_types.h"
  20. #include "data/data_message_reaction_id.h"
  21. #include "data/data_peer_values.h"
  22. #include "lang/lang_keys.h"
  23. #include "main/main_app_config.h"
  24. #include "main/main_session.h"
  25. #include "base/unixtime.h"
  26. #include "base/weak_ptr.h"
  27. #include "ui/controls/who_reacted_context_action.h"
  28. #include "apiwrap.h"
  29. #include "styles/style_chat.h"
  30. #include "styles/style_chat_helpers.h"
  31. namespace Api {
  32. namespace {
  33. constexpr auto kContextReactionsLimit = 50;
  34. using Data::ReactionId;
  35. using WhoReadState = Ui::WhoReadState;
  36. struct Peers {
  37. std::vector<WhoReadPeer> list;
  38. WhoReadState state = WhoReadState::Empty;
  39. friend inline bool operator==(
  40. const Peers &a,
  41. const Peers &b) noexcept = default;
  42. };
  43. struct PeerWithReaction {
  44. WhoReadPeer peerWithDate;
  45. ReactionId reaction;
  46. friend inline bool operator==(
  47. const PeerWithReaction &a,
  48. const PeerWithReaction &b) noexcept = default;
  49. };
  50. struct PeersWithReactions {
  51. std::vector<PeerWithReaction> list;
  52. std::vector<WhoReadPeer> read;
  53. int fullReactionsCount = 0;
  54. WhoReadState state = WhoReadState::Empty;
  55. friend inline bool operator==(
  56. const PeersWithReactions &a,
  57. const PeersWithReactions &b) noexcept = default;
  58. };
  59. struct CachedRead {
  60. CachedRead()
  61. : data(Peers{ .state = WhoReadState::Unknown }) {
  62. }
  63. rpl::variable<Peers> data;
  64. mtpRequestId requestId = 0;
  65. };
  66. struct CachedReacted {
  67. CachedReacted()
  68. : data(PeersWithReactions{ .state = WhoReadState::Unknown }) {
  69. }
  70. rpl::variable<PeersWithReactions> data;
  71. mtpRequestId requestId = 0;
  72. };
  73. struct Context {
  74. base::flat_map<not_null<HistoryItem*>, CachedRead> cachedRead;
  75. base::flat_map<
  76. not_null<HistoryItem*>,
  77. base::flat_map<ReactionId, CachedReacted>> cachedReacted;
  78. base::flat_map<not_null<Main::Session*>, rpl::lifetime> subscriptions;
  79. [[nodiscard]] CachedRead &cacheRead(not_null<HistoryItem*> item) {
  80. const auto i = cachedRead.find(item);
  81. if (i != end(cachedRead)) {
  82. return i->second;
  83. }
  84. return cachedRead.emplace(item, CachedRead()).first->second;
  85. }
  86. [[nodiscard]] CachedReacted &cacheReacted(
  87. not_null<HistoryItem*> item,
  88. const ReactionId &reaction) {
  89. auto &map = cachedReacted[item];
  90. const auto i = map.find(reaction);
  91. if (i != end(map)) {
  92. return i->second;
  93. }
  94. return map.emplace(reaction, CachedReacted()).first->second;
  95. }
  96. };
  97. struct Userpic {
  98. not_null<PeerData*> peer;
  99. TimeId date = 0;
  100. bool dateReacted = false;
  101. QString customEntityData;
  102. mutable Ui::PeerUserpicView view;
  103. mutable InMemoryKey uniqueKey;
  104. };
  105. struct State {
  106. std::vector<Userpic> userpics;
  107. Ui::WhoReadContent current;
  108. base::has_weak_ptr guard;
  109. bool someUserpicsNotLoaded = false;
  110. bool scheduled = false;
  111. };
  112. [[nodiscard]] auto Contexts()
  113. -> base::flat_map<not_null<QWidget*>, std::unique_ptr<Context>> & {
  114. static auto result = base::flat_map<
  115. not_null<QWidget*>,
  116. std::unique_ptr<Context>>();
  117. return result;
  118. }
  119. [[nodiscard]] not_null<Context*> ContextAt(not_null<QWidget*> key) {
  120. auto &contexts = Contexts();
  121. const auto i = contexts.find(key);
  122. if (i != end(contexts)) {
  123. return i->second.get();
  124. }
  125. const auto result = contexts.emplace(
  126. key,
  127. std::make_unique<Context>()).first->second.get();
  128. QObject::connect(key.get(), &QObject::destroyed, [=] {
  129. auto &contexts = Contexts();
  130. const auto i = contexts.find(key);
  131. for (auto &[item, entry] : i->second->cachedRead) {
  132. if (const auto requestId = entry.requestId) {
  133. item->history()->session().api().request(requestId).cancel();
  134. }
  135. }
  136. for (auto &[item, map] : i->second->cachedReacted) {
  137. for (auto &[reaction, entry] : map) {
  138. if (const auto requestId = entry.requestId) {
  139. item->history()->session().api().request(requestId).cancel();
  140. }
  141. }
  142. }
  143. contexts.erase(i);
  144. });
  145. return result;
  146. }
  147. [[nodiscard]] not_null<Context*> PreparedContextAt(
  148. not_null<QWidget*> key,
  149. not_null<Main::Session*> session) {
  150. const auto context = ContextAt(key);
  151. if (context->subscriptions.contains(session)) {
  152. return context;
  153. }
  154. session->changes().messageUpdates(
  155. Data::MessageUpdate::Flag::Destroyed
  156. ) | rpl::start_with_next([=](const Data::MessageUpdate &update) {
  157. const auto i = context->cachedRead.find(update.item);
  158. if (i != end(context->cachedRead)) {
  159. session->api().request(i->second.requestId).cancel();
  160. context->cachedRead.erase(i);
  161. }
  162. const auto j = context->cachedReacted.find(update.item);
  163. if (j != end(context->cachedReacted)) {
  164. for (auto &[reaction, entry] : j->second) {
  165. session->api().request(entry.requestId).cancel();
  166. }
  167. context->cachedReacted.erase(j);
  168. }
  169. }, context->subscriptions[session]);
  170. Data::AmPremiumValue(
  171. session
  172. ) | rpl::skip(1) | rpl::filter(
  173. rpl::mappers::_1
  174. ) | rpl::start_with_next([=] {
  175. for (auto &[item, cache] : context->cachedRead) {
  176. if (cache.data.current().state == Ui::WhoReadState::MyHidden) {
  177. cache.data = Peers{ .state = Ui::WhoReadState::Unknown };
  178. }
  179. }
  180. }, context->subscriptions[session]);
  181. session->api().globalPrivacy().hideReadTime(
  182. ) | rpl::skip(1) | rpl::filter(
  183. !rpl::mappers::_1
  184. ) | rpl::start_with_next([=] {
  185. for (auto &[item, cache] : context->cachedRead) {
  186. if (cache.data.current().state == Ui::WhoReadState::MyHidden) {
  187. cache.data = Peers{ .state = Ui::WhoReadState::Unknown };
  188. }
  189. }
  190. }, context->subscriptions[session]);
  191. return context;
  192. }
  193. [[nodiscard]] QImage GenerateUserpic(Userpic &userpic, int size) {
  194. size *= style::DevicePixelRatio();
  195. auto result = PeerData::GenerateUserpicImage(
  196. userpic.peer,
  197. userpic.view,
  198. size);
  199. result.setDevicePixelRatio(style::DevicePixelRatio());
  200. return result;
  201. }
  202. [[nodiscard]] Ui::WhoReadType DetectSeenType(not_null<HistoryItem*> item) {
  203. if (const auto media = item->media()) {
  204. if (!media->webpage()) {
  205. if (const auto document = media->document()) {
  206. if (document->isVoiceMessage()) {
  207. return Ui::WhoReadType::Listened;
  208. } else if (document->isVideoMessage()) {
  209. return Ui::WhoReadType::Watched;
  210. }
  211. }
  212. }
  213. }
  214. return Ui::WhoReadType::Seen;
  215. }
  216. [[nodiscard]] rpl::producer<Peers> WhoReadIds(
  217. not_null<HistoryItem*> item,
  218. not_null<QWidget*> context) {
  219. auto weak = QPointer<QWidget>(context.get());
  220. const auto session = &item->history()->session();
  221. return [=](auto consumer) {
  222. if (!weak) {
  223. return rpl::lifetime();
  224. }
  225. const auto context = PreparedContextAt(weak.data(), session);
  226. auto &entry = context->cacheRead(item);
  227. if (entry.requestId) {
  228. } else if (const auto user = item->history()->peer->asUser()) {
  229. entry.requestId = session->api().request(
  230. MTPmessages_GetOutboxReadDate(
  231. user->input,
  232. MTP_int(item->id)
  233. )
  234. ).done([=](const MTPOutboxReadDate &result) {
  235. const auto &data = result.data();
  236. auto &entry = context->cacheRead(item);
  237. entry.requestId = 0;
  238. auto parsed = Peers();
  239. parsed.list.push_back({
  240. .peer = user->id,
  241. .date = data.vdate().v,
  242. });
  243. entry.data = std::move(parsed);
  244. }).fail([=](const MTP::Error &error) {
  245. auto &entry = context->cacheRead(item);
  246. entry.requestId = 0;
  247. if (entry.data.current().state == WhoReadState::Unknown) {
  248. const auto &text = error.type();
  249. entry.data = (text == u"YOUR_PRIVACY_RESTRICTED"_q)
  250. ? Peers{ .state = WhoReadState::MyHidden }
  251. : (text == u"USER_PRIVACY_RESTRICTED"_q)
  252. ? Peers{ .state = WhoReadState::HisHidden }
  253. : (text == u"MESSAGE_TOO_OLD"_q)
  254. ? Peers{ .state = WhoReadState::TooOld }
  255. : Peers{ .state = WhoReadState::Empty };
  256. }
  257. }).send();
  258. } else {
  259. entry.requestId = session->api().request(
  260. MTPmessages_GetMessageReadParticipants(
  261. item->history()->peer->input,
  262. MTP_int(item->id)
  263. )
  264. ).done([=](const MTPVector<MTPReadParticipantDate> &result) {
  265. auto &entry = context->cacheRead(item);
  266. entry.requestId = 0;
  267. auto parsed = Peers();
  268. parsed.list.reserve(result.v.size());
  269. for (const auto &id : result.v) {
  270. parsed.list.push_back({
  271. .peer = UserId(id.data().vuser_id()),
  272. .date = id.data().vdate().v,
  273. });
  274. }
  275. entry.data = std::move(parsed);
  276. }).fail([=] {
  277. auto &entry = context->cacheRead(item);
  278. entry.requestId = 0;
  279. if (entry.data.current().state == WhoReadState::Unknown) {
  280. entry.data = Peers{ .state = WhoReadState::Empty };
  281. }
  282. }).send();
  283. }
  284. return entry.data.value().start_existing(consumer);
  285. };
  286. }
  287. [[nodiscard]] PeersWithReactions WithEmptyReactions(
  288. Peers &&peers) {
  289. auto result = PeersWithReactions{
  290. .list = peers.list | ranges::views::transform([](WhoReadPeer peer) {
  291. return PeerWithReaction{ .peerWithDate = peer };
  292. }) | ranges::to_vector,
  293. .state = peers.state,
  294. };
  295. result.read = std::move(peers.list);
  296. return result;
  297. }
  298. [[nodiscard]] rpl::producer<PeersWithReactions> WhoReactedIds(
  299. not_null<HistoryItem*> item,
  300. const ReactionId &reaction,
  301. not_null<QWidget*> context) {
  302. auto weak = QPointer<QWidget>(context.get());
  303. const auto session = &item->history()->session();
  304. return [=](auto consumer) {
  305. if (!weak) {
  306. return rpl::lifetime();
  307. }
  308. const auto context = PreparedContextAt(weak.data(), session);
  309. auto &entry = context->cacheReacted(item, reaction);
  310. if (!entry.requestId) {
  311. using Flag = MTPmessages_GetMessageReactionsList::Flag;
  312. entry.requestId = session->api().request(
  313. MTPmessages_GetMessageReactionsList(
  314. MTP_flags(reaction.empty()
  315. ? Flag(0)
  316. : Flag::f_reaction),
  317. item->history()->peer->input,
  318. MTP_int(item->id),
  319. ReactionToMTP(reaction),
  320. MTPstring(), // offset
  321. MTP_int(kContextReactionsLimit)
  322. )
  323. ).done([=](const MTPmessages_MessageReactionsList &result) {
  324. auto &entry = context->cacheReacted(item, reaction);
  325. entry.requestId = 0;
  326. result.match([&](
  327. const MTPDmessages_messageReactionsList &data) {
  328. session->data().processUsers(data.vusers());
  329. session->data().processChats(data.vchats());
  330. auto parsed = PeersWithReactions{
  331. .fullReactionsCount = data.vcount().v,
  332. };
  333. parsed.list.reserve(data.vreactions().v.size());
  334. for (const auto &vote : data.vreactions().v) {
  335. const auto &data = vote.data();
  336. parsed.list.push_back(PeerWithReaction{
  337. .peerWithDate = {
  338. .peer = peerFromMTP(data.vpeer_id()),
  339. .date = data.vdate().v,
  340. .dateReacted = true,
  341. },
  342. .reaction = Data::ReactionFromMTP(
  343. data.vreaction()),
  344. });
  345. }
  346. entry.data = std::move(parsed);
  347. });
  348. }).fail([=] {
  349. auto &entry = context->cacheReacted(item, reaction);
  350. entry.requestId = 0;
  351. if (entry.data.current().state == WhoReadState::Unknown) {
  352. entry.data = PeersWithReactions{
  353. .state = WhoReadState::Empty,
  354. };
  355. }
  356. }).send();
  357. }
  358. return entry.data.value().start_existing(consumer);
  359. };
  360. }
  361. [[nodiscard]] auto WhoReadOrReactedIds(
  362. not_null<HistoryItem*> item,
  363. not_null<QWidget*> context)
  364. -> rpl::producer<PeersWithReactions> {
  365. return rpl::combine(
  366. WhoReactedIds(item, {}, context),
  367. WhoReadIds(item, context)
  368. ) | rpl::map([=](PeersWithReactions &&reacted, Peers &&read) {
  369. if (reacted.state == WhoReadState::Unknown
  370. || read.state == WhoReadState::Unknown) {
  371. return PeersWithReactions{ .state = WhoReadState::Unknown};
  372. }
  373. auto &list = reacted.list;
  374. for (const auto &peerWithDate : read.list) {
  375. const auto i = ranges::find(
  376. list,
  377. peerWithDate.peer,
  378. [](const PeerWithReaction &p) {
  379. return p.peerWithDate.peer; });
  380. if (i == end(list)) {
  381. list.push_back({ .peerWithDate = peerWithDate });
  382. } else if (!i->peerWithDate.date) {
  383. i->peerWithDate.date = peerWithDate.date;
  384. i->peerWithDate.dateReacted = peerWithDate.dateReacted;
  385. }
  386. }
  387. reacted.read = std::move(read.list);
  388. return std::move(reacted);
  389. });
  390. }
  391. bool UpdateUserpics(
  392. not_null<State*> state,
  393. not_null<HistoryItem*> item,
  394. const std::vector<PeerWithReaction> &ids) {
  395. auto &owner = item->history()->owner();
  396. struct ResolvedPeer {
  397. PeerData *peer = nullptr;
  398. TimeId date = 0;
  399. bool dateReacted = false;
  400. ReactionId reaction;
  401. };
  402. const auto peers = ranges::views::all(
  403. ids
  404. ) | ranges::views::transform([&](PeerWithReaction id) {
  405. return ResolvedPeer{
  406. .peer = owner.peerLoaded(id.peerWithDate.peer),
  407. .date = id.peerWithDate.date,
  408. .dateReacted = id.peerWithDate.dateReacted,
  409. .reaction = id.reaction,
  410. };
  411. }) | ranges::views::filter([](ResolvedPeer resolved) {
  412. return resolved.peer != nullptr;
  413. }) | ranges::to_vector;
  414. const auto same = ranges::equal(
  415. state->userpics,
  416. peers,
  417. ranges::equal_to(),
  418. [](const Userpic &u) { return std::pair(u.peer.get(), u.date); },
  419. [](const ResolvedPeer &r) { return std::pair(r.peer, r.date); });
  420. if (same) {
  421. return false;
  422. }
  423. auto &was = state->userpics;
  424. auto now = std::vector<Userpic>();
  425. for (const auto &resolved : peers) {
  426. const auto peer = not_null{ resolved.peer };
  427. const auto &data = ReactionEntityData(resolved.reaction);
  428. const auto i = ranges::find(was, peer, &Userpic::peer);
  429. if (i != end(was) && i->view.cloud) {
  430. i->date = resolved.date;
  431. i->dateReacted = resolved.dateReacted;
  432. now.push_back(std::move(*i));
  433. now.back().customEntityData = data;
  434. continue;
  435. }
  436. now.push_back(Userpic{
  437. .peer = peer,
  438. .date = resolved.date,
  439. .dateReacted = resolved.dateReacted,
  440. .customEntityData = data,
  441. });
  442. auto &userpic = now.back();
  443. userpic.uniqueKey = peer->userpicUniqueKey(userpic.view);
  444. peer->loadUserpic();
  445. }
  446. was = std::move(now);
  447. return true;
  448. }
  449. void RegenerateUserpics(not_null<State*> state, int small, int large) {
  450. Expects(state->userpics.size() == state->current.participants.size());
  451. state->someUserpicsNotLoaded = false;
  452. const auto count = int(state->userpics.size());
  453. for (auto i = 0; i != count; ++i) {
  454. auto &userpic = state->userpics[i];
  455. auto &participant = state->current.participants[i];
  456. const auto peer = userpic.peer;
  457. const auto key = peer->userpicUniqueKey(userpic.view);
  458. if (peer->hasUserpic() && peer->useEmptyUserpic(userpic.view)) {
  459. state->someUserpicsNotLoaded = true;
  460. }
  461. if (userpic.uniqueKey == key) {
  462. continue;
  463. }
  464. participant.userpicKey = userpic.uniqueKey = key;
  465. participant.userpicLarge = GenerateUserpic(userpic, large);
  466. if (i < Ui::WhoReadParticipant::kMaxSmallUserpics) {
  467. participant.userpicSmall = GenerateUserpic(userpic, small);
  468. }
  469. }
  470. }
  471. void RegenerateParticipants(not_null<State*> state, int small, int large) {
  472. const auto currentDate = QDateTime::currentDateTime();
  473. auto old = base::take(state->current.participants);
  474. auto &now = state->current.participants;
  475. now.reserve(state->userpics.size());
  476. for (auto &userpic : state->userpics) {
  477. const auto peer = userpic.peer;
  478. const auto date = userpic.date;
  479. const auto id = peer->id.value;
  480. const auto was = ranges::find(old, id, &Ui::WhoReadParticipant::id);
  481. if (was != end(old)) {
  482. was->name = peer->name();
  483. was->date = FormatReadDate(date, currentDate);
  484. was->dateReacted = userpic.dateReacted;
  485. now.push_back(std::move(*was));
  486. continue;
  487. }
  488. now.push_back({
  489. .name = peer->name(),
  490. .date = FormatReadDate(date, currentDate),
  491. .dateReacted = userpic.dateReacted,
  492. .customEntityData = userpic.customEntityData,
  493. .userpicLarge = GenerateUserpic(userpic, large),
  494. .userpicKey = userpic.uniqueKey,
  495. .id = id,
  496. });
  497. if (now.size() <= Ui::WhoReadParticipant::kMaxSmallUserpics) {
  498. now.back().userpicSmall = GenerateUserpic(userpic, small);
  499. }
  500. }
  501. RegenerateUserpics(state, small, large);
  502. }
  503. rpl::producer<Ui::WhoReadContent> WhoReacted(
  504. not_null<HistoryItem*> item,
  505. const ReactionId &reaction,
  506. not_null<QWidget*> context,
  507. const style::WhoRead &st,
  508. std::shared_ptr<WhoReadList> whoReadIds) {
  509. const auto small = st.userpics.size;
  510. const auto large = st.photoSize;
  511. return [=](auto consumer) {
  512. auto lifetime = rpl::lifetime();
  513. const auto resolveWhoRead = reaction.empty()
  514. && WhoReadExists(item);
  515. const auto state = lifetime.make_state<State>();
  516. const auto pushNext = [=] {
  517. consumer.put_next_copy(state->current);
  518. };
  519. const auto resolveWhoReacted = !reaction.empty()
  520. || item->canViewReactions();
  521. auto idsWithReactions = (resolveWhoRead && resolveWhoReacted)
  522. ? WhoReadOrReactedIds(item, context)
  523. : resolveWhoRead
  524. ? (WhoReadIds(item, context) | rpl::map(WithEmptyReactions))
  525. : WhoReactedIds(item, reaction, context);
  526. state->current.type = resolveWhoRead
  527. ? DetectSeenType(item)
  528. : Ui::WhoReadType::Reacted;
  529. if (resolveWhoReacted) {
  530. const auto &list = item->reactions();
  531. state->current.fullReactionsCount = [&] {
  532. if (reaction.empty()) {
  533. return ranges::accumulate(
  534. list,
  535. 0,
  536. ranges::plus{},
  537. &Data::MessageReaction::count);
  538. }
  539. const auto i = ranges::find(
  540. list,
  541. reaction,
  542. &Data::MessageReaction::id);
  543. return (i != end(list)) ? i->count : 0;
  544. }();
  545. state->current.singleCustomEntityData = ReactionEntityData(
  546. !reaction.empty()
  547. ? reaction
  548. : (list.size() == 1)
  549. ? list.front().id
  550. : ReactionId());
  551. }
  552. std::move(
  553. idsWithReactions
  554. ) | rpl::start_with_next([=](PeersWithReactions &&peers) {
  555. if (peers.state == WhoReadState::Unknown) {
  556. state->userpics.clear();
  557. consumer.put_next(Ui::WhoReadContent{
  558. .type = state->current.type,
  559. .fullReactionsCount = state->current.fullReactionsCount,
  560. .fullReadCount = state->current.fullReadCount,
  561. .state = WhoReadState::Unknown,
  562. });
  563. return;
  564. }
  565. state->current.state = peers.state;
  566. state->current.fullReadCount = int(peers.read.size());
  567. state->current.fullReactionsCount = peers.fullReactionsCount;
  568. if (whoReadIds) {
  569. const auto reacted = peers.list.size() - ranges::count(
  570. peers.list,
  571. ReactionId(),
  572. &PeerWithReaction::reaction);
  573. whoReadIds->list = (peers.read.size() > reacted)
  574. ? std::move(peers.read)
  575. : std::vector<WhoReadPeer>();
  576. }
  577. if (UpdateUserpics(state, item, peers.list)) {
  578. RegenerateParticipants(state, small, large);
  579. pushNext();
  580. } else if (peers.list.empty()) {
  581. pushNext();
  582. }
  583. }, lifetime);
  584. item->history()->session().downloaderTaskFinished(
  585. ) | rpl::filter([=] {
  586. return state->someUserpicsNotLoaded && !state->scheduled;
  587. }) | rpl::start_with_next([=] {
  588. for (const auto &userpic : state->userpics) {
  589. if (userpic.peer->userpicUniqueKey(userpic.view)
  590. != userpic.uniqueKey) {
  591. state->scheduled = true;
  592. crl::on_main(&state->guard, [=] {
  593. state->scheduled = false;
  594. RegenerateUserpics(state, small, large);
  595. pushNext();
  596. });
  597. return;
  598. }
  599. }
  600. }, lifetime);
  601. return lifetime;
  602. };
  603. }
  604. } // namespace
  605. QString FormatReadDate(TimeId date, const QDateTime &now) {
  606. if (!date) {
  607. return {};
  608. }
  609. const auto parsed = base::unixtime::parse(date);
  610. const auto readDate = parsed.date();
  611. const auto nowDate = now.date();
  612. if (readDate == nowDate) {
  613. return tr::lng_mediaview_today(
  614. tr::now,
  615. lt_time,
  616. QLocale().toString(parsed.time(), QLocale::ShortFormat));
  617. } else if (readDate.addDays(1) == nowDate) {
  618. return tr::lng_mediaview_yesterday(
  619. tr::now,
  620. lt_time,
  621. QLocale().toString(parsed.time(), QLocale::ShortFormat));
  622. }
  623. return tr::lng_mediaview_date_time(
  624. tr::now,
  625. lt_date,
  626. tr::lng_month_day(
  627. tr::now,
  628. lt_month,
  629. Lang::MonthDay(readDate.month())(tr::now),
  630. lt_day,
  631. QString::number(readDate.day())),
  632. lt_time,
  633. QLocale().toString(parsed.time(), QLocale::ShortFormat));
  634. }
  635. bool WhoReadExists(not_null<HistoryItem*> item) {
  636. if (!item->out()) {
  637. return false;
  638. }
  639. const auto type = DetectSeenType(item);
  640. const auto thread = item->topic()
  641. ? (Data::Thread*)item->topic()
  642. : item->history();
  643. const auto unseen = (type == Ui::WhoReadType::Seen)
  644. ? item->unread(thread)
  645. : item->isUnreadMedia();
  646. if (unseen) {
  647. return false;
  648. }
  649. const auto history = item->history();
  650. const auto peer = history->peer;
  651. if (const auto user = peer->asUser()) {
  652. if (user->isSelf()
  653. || user->isBot()
  654. || user->isServiceUser()
  655. || user->readDatesPrivate()) {
  656. return false;
  657. }
  658. const auto &appConfig = peer->session().appConfig();
  659. const auto expirePeriod = appConfig.get<int>(
  660. "pm_read_date_expire_period",
  661. 7 * 86400);
  662. if (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) {
  663. return false;
  664. }
  665. return true;
  666. }
  667. const auto chat = peer->asChat();
  668. const auto megagroup = peer->asMegagroup();
  669. if ((!chat && !megagroup)
  670. || (megagroup
  671. && (megagroup->flags() & ChannelDataFlag::ParticipantsHidden))) {
  672. return false;
  673. }
  674. const auto &appConfig = peer->session().appConfig();
  675. const auto expirePeriod = appConfig.get<int>(
  676. "chat_read_mark_expire_period",
  677. 7 * 86400);
  678. if (item->date() + int64(expirePeriod) <= int64(base::unixtime::now())) {
  679. return false;
  680. }
  681. const auto maxCount = appConfig.get<int>(
  682. "chat_read_mark_size_threshold",
  683. 50);
  684. const auto count = megagroup ? megagroup->membersCount() : chat->count;
  685. if (count <= 0 || count > maxCount) {
  686. return false;
  687. }
  688. return true;
  689. }
  690. bool WhoReactedExists(
  691. not_null<HistoryItem*> item,
  692. WhoReactedList list) {
  693. if (item->canViewReactions() || WhoReadExists(item)) {
  694. return true;
  695. }
  696. return (list == WhoReactedList::One) && item->history()->peer->isUser();
  697. }
  698. rpl::producer<Ui::WhoReadContent> WhoReacted(
  699. not_null<HistoryItem*> item,
  700. not_null<QWidget*> context,
  701. const style::WhoRead &st,
  702. std::shared_ptr<WhoReadList> whoReadIds) {
  703. return WhoReacted(item, {}, context, st, std::move(whoReadIds));
  704. }
  705. rpl::producer<Ui::WhoReadContent> WhoReacted(
  706. not_null<HistoryItem*> item,
  707. const Data::ReactionId &reaction,
  708. not_null<QWidget*> context,
  709. const style::WhoRead &st) {
  710. return WhoReacted(item, reaction, context, st, nullptr);
  711. }
  712. [[nodiscard]] rpl::producer<Ui::WhoReadContent> WhenDate(
  713. not_null<PeerData*> author,
  714. TimeId date,
  715. Ui::WhoReadType type) {
  716. return rpl::single(Ui::WhoReadContent{
  717. .participants = { Ui::WhoReadParticipant{
  718. .name = author->name(),
  719. .date = FormatReadDate(date, QDateTime::currentDateTime()),
  720. .id = author->id.value,
  721. } },
  722. .type = type,
  723. .fullReadCount = 1,
  724. });
  725. }
  726. rpl::producer<Ui::WhoReadContent> WhenEdited(
  727. not_null<PeerData*> author,
  728. TimeId date) {
  729. return WhenDate(author, date, Ui::WhoReadType::Edited);
  730. }
  731. rpl::producer<Ui::WhoReadContent> WhenOriginal(
  732. not_null<PeerData*> author,
  733. TimeId date) {
  734. return WhenDate(author, date, Ui::WhoReadType::Original);
  735. }
  736. } // namespace Api