history_item_helpers.cpp 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243
  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 "history/history_item_helpers.h"
  8. #include "api/api_text_entities.h"
  9. #include "boxes/premium_preview_box.h"
  10. #include "calls/calls_instance.h"
  11. #include "data/components/sponsored_messages.h"
  12. #include "data/stickers/data_custom_emoji.h"
  13. #include "data/notify/data_notify_settings.h"
  14. #include "data/data_channel.h"
  15. #include "data/data_chat.h"
  16. #include "data/data_changes.h"
  17. #include "data/data_document.h"
  18. #include "data/data_group_call.h"
  19. #include "data/data_forum.h"
  20. #include "data/data_forum_topic.h"
  21. #include "data/data_message_reactions.h"
  22. #include "data/data_session.h"
  23. #include "data/data_stories.h"
  24. #include "data/data_user.h"
  25. #include "history/history.h"
  26. #include "history/history_item_components.h"
  27. #include "main/main_account.h"
  28. #include "main/main_domain.h"
  29. #include "main/main_session.h"
  30. #include "main/main_session_settings.h"
  31. #include "menu/menu_sponsored.h"
  32. #include "platform/platform_notifications_manager.h"
  33. #include "window/window_controller.h"
  34. #include "window/window_session_controller.h"
  35. #include "apiwrap.h"
  36. #include "base/unixtime.h"
  37. #include "core/application.h"
  38. #include "core/click_handler_types.h" // ClickHandlerContext.
  39. #include "settings/settings_credits_graphics.h"
  40. #include "storage/storage_account.h"
  41. #include "ui/boxes/confirm_box.h"
  42. #include "ui/text/format_values.h"
  43. #include "ui/text/text_utilities.h"
  44. #include "ui/toast/toast.h"
  45. #include "ui/widgets/checkbox.h"
  46. #include "ui/item_text_options.h"
  47. #include "lang/lang_keys.h"
  48. namespace {
  49. bool PeerCallKnown(not_null<PeerData*> peer) {
  50. if (peer->groupCall() != nullptr) {
  51. return true;
  52. } else if (const auto chat = peer->asChat()) {
  53. return !(chat->flags() & ChatDataFlag::CallActive);
  54. } else if (const auto channel = peer->asChannel()) {
  55. return !(channel->flags() & ChannelDataFlag::CallActive);
  56. }
  57. return true;
  58. }
  59. } // namespace
  60. int ComputeSendingMessagesCount(
  61. not_null<History*> history,
  62. const SendingErrorRequest &request) {
  63. auto result = 0;
  64. if (request.text && !request.text->empty()) {
  65. auto sending = TextWithEntities();
  66. auto left = TextWithEntities{
  67. request.text->text,
  68. TextUtilities::ConvertTextTagsToEntities(request.text->tags)
  69. };
  70. auto prepareFlags = Ui::ItemTextOptions(
  71. history,
  72. history->session().user()).flags;
  73. TextUtilities::PrepareForSending(left, prepareFlags);
  74. while (TextUtilities::CutPart(sending, left, MaxMessageSize)) {
  75. ++result;
  76. }
  77. if (!result) {
  78. ++result;
  79. }
  80. }
  81. return result
  82. + (request.story ? 1 : 0)
  83. + (request.forward ? int(request.forward->size()) : 0);
  84. }
  85. Data::SendError GetErrorForSending(
  86. not_null<PeerData*> peer,
  87. SendingErrorRequest request) {
  88. const auto forum = request.topicRootId ? peer->forum() : nullptr;
  89. const auto topic = forum
  90. ? forum->topicFor(request.topicRootId)
  91. : nullptr;
  92. const auto thread = topic
  93. ? not_null<Data::Thread*>(topic)
  94. : peer->owner().history(peer);
  95. if (request.story) {
  96. if (const auto error = request.story->errorTextForForward(thread)) {
  97. return error;
  98. }
  99. }
  100. if (request.forward) {
  101. for (const auto &item : *request.forward) {
  102. if (const auto error = item->errorTextForForward(thread)) {
  103. return error;
  104. }
  105. }
  106. }
  107. const auto hasText = (request.text && !request.text->empty());
  108. if (hasText) {
  109. const auto error = Data::RestrictionError(
  110. peer,
  111. ChatRestriction::SendOther);
  112. if (error) {
  113. return error;
  114. } else if (!Data::CanSendTexts(thread)) {
  115. return tr::lng_forward_cant(tr::now);
  116. }
  117. }
  118. if (peer->slowmodeApplied()) {
  119. const auto count = request.messagesCount
  120. ? request.messagesCount
  121. : ComputeSendingMessagesCount(thread->owningHistory(), request);
  122. if (const auto history = peer->owner().historyLoaded(peer)) {
  123. if (!request.ignoreSlowmodeCountdown
  124. && (history->latestSendingMessage() != nullptr)
  125. && (count > 0)) {
  126. return tr::lng_slowmode_no_many(tr::now);
  127. }
  128. }
  129. if (request.text && request.text->text.size() > MaxMessageSize) {
  130. return tr::lng_slowmode_too_long(tr::now);
  131. } else if ((hasText || request.story) && count > 1) {
  132. return tr::lng_slowmode_no_many(tr::now);
  133. } else if (count > 1) {
  134. const auto albumForward = [&] {
  135. const auto first = request.forward->front();
  136. if (const auto groupId = first->groupId()) {
  137. for (const auto &item : *request.forward) {
  138. if (item->groupId() != groupId) {
  139. return false;
  140. }
  141. }
  142. return true;
  143. }
  144. return false;
  145. }();
  146. if (!albumForward) {
  147. return tr::lng_slowmode_no_many(tr::now);
  148. }
  149. }
  150. }
  151. if (const auto left = peer->slowmodeSecondsLeft()) {
  152. if (!request.ignoreSlowmodeCountdown) {
  153. return tr::lng_slowmode_enabled(
  154. tr::now,
  155. lt_left,
  156. Ui::FormatDurationWordsSlowmode(left));
  157. }
  158. }
  159. return {};
  160. }
  161. Data::SendError GetErrorForSending(
  162. not_null<Data::Thread*> thread,
  163. SendingErrorRequest request) {
  164. request.topicRootId = thread->topicRootId();
  165. return GetErrorForSending(thread->peer(), std::move(request));
  166. }
  167. Data::SendErrorWithThread GetErrorForSending(
  168. const std::vector<not_null<Data::Thread*>> &threads,
  169. SendingErrorRequest request) {
  170. for (const auto thread : threads) {
  171. const auto error = GetErrorForSending(thread, request);
  172. if (error) {
  173. return Data::SendErrorWithThread{ error, thread };
  174. }
  175. }
  176. return {};
  177. }
  178. std::optional<SendPaymentDetails> ComputePaymentDetails(
  179. not_null<PeerData*> peer,
  180. int messagesCount) {
  181. if (const auto user = peer->asUser()) {
  182. if (user->hasStarsPerMessage()
  183. && !user->messageMoneyRestrictionsKnown()) {
  184. user->updateFull();
  185. return {};
  186. }
  187. } else if (const auto channel = peer->asChannel()) {
  188. if (!channel->isFullLoaded()) {
  189. channel->updateFull();
  190. return {};
  191. }
  192. }
  193. if (!peer->session().credits().loaded()) {
  194. peer->session().credits().load();
  195. return {};
  196. } else if (const auto perMessage = peer->starsPerMessageChecked()) {
  197. return SendPaymentDetails{
  198. .messages = messagesCount,
  199. .stars = messagesCount * perMessage,
  200. };
  201. }
  202. return SendPaymentDetails();
  203. }
  204. object_ptr<Ui::BoxContent> MakeSendErrorBox(
  205. const Data::SendErrorWithThread &error,
  206. bool withTitle) {
  207. Expects(error.error.has_value() && error.thread != nullptr);
  208. auto text = TextWithEntities();
  209. if (withTitle) {
  210. text.append(
  211. Ui::Text::Bold(error.thread->chatListName())
  212. ).append("\n\n");
  213. }
  214. if (error.error.boostsToLift) {
  215. text.append(Ui::Text::Link(error.error.text));
  216. } else {
  217. text.append(error.error.text);
  218. }
  219. const auto peer = error.thread->peer();
  220. const auto lifting = error.error.boostsToLift;
  221. const auto filter = [=](const auto &...) {
  222. Expects(peer->isChannel());
  223. const auto window = ChatHelpers::ResolveWindowDefault()(
  224. &peer->session());
  225. window->resolveBoostState(peer->asChannel(), lifting);
  226. return false;
  227. };
  228. return Ui::MakeInformBox({
  229. .text = text,
  230. .labelFilter = filter,
  231. });
  232. }
  233. void ShowSendPaidConfirm(
  234. not_null<Window::SessionNavigation*> navigation,
  235. not_null<PeerData*> peer,
  236. SendPaymentDetails details,
  237. Fn<void()> confirmed,
  238. PaidConfirmStyles styles) {
  239. return ShowSendPaidConfirm(
  240. navigation->uiShow(),
  241. peer,
  242. details,
  243. confirmed,
  244. styles);
  245. }
  246. void ShowSendPaidConfirm(
  247. std::shared_ptr<Main::SessionShow> show,
  248. not_null<PeerData*> peer,
  249. SendPaymentDetails details,
  250. Fn<void()> confirmed,
  251. PaidConfirmStyles styles) {
  252. ShowSendPaidConfirm(
  253. std::move(show),
  254. std::vector<not_null<PeerData*>>{ peer },
  255. details,
  256. confirmed,
  257. styles);
  258. }
  259. void ShowSendPaidConfirm(
  260. std::shared_ptr<Main::SessionShow> show,
  261. const std::vector<not_null<PeerData*>> &peers,
  262. SendPaymentDetails details,
  263. Fn<void()> confirmed,
  264. PaidConfirmStyles styles) {
  265. Expects(!peers.empty());
  266. const auto singlePeer = (peers.size() > 1)
  267. ? (PeerData*)nullptr
  268. : peers.front().get();
  269. const auto singlePeerId = singlePeer ? singlePeer->id : PeerId();
  270. const auto check = [=] {
  271. const auto required = details.stars;
  272. if (!required) {
  273. return;
  274. }
  275. const auto done = [=](Settings::SmallBalanceResult result) {
  276. if (result == Settings::SmallBalanceResult::Success
  277. || result == Settings::SmallBalanceResult::Already) {
  278. confirmed();
  279. }
  280. };
  281. Settings::MaybeRequestBalanceIncrease(
  282. show,
  283. required,
  284. Settings::SmallBalanceForMessage{ .recipientId = singlePeerId },
  285. done);
  286. };
  287. auto usersOnly = true;
  288. for (const auto &peer : peers) {
  289. if (!peer->isUser()) {
  290. usersOnly = false;
  291. break;
  292. }
  293. }
  294. const auto singlePeerStars = singlePeer
  295. ? singlePeer->starsPerMessageChecked()
  296. : 0;
  297. if (singlePeer) {
  298. const auto session = &singlePeer->session();
  299. const auto trusted = session->local().isPeerTrustedPayForMessage(
  300. singlePeerId,
  301. singlePeerStars);
  302. if (trusted) {
  303. check();
  304. return;
  305. }
  306. }
  307. const auto messages = details.messages;
  308. const auto stars = details.stars;
  309. show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
  310. const auto trust = std::make_shared<QPointer<Ui::Checkbox>>();
  311. const auto proceed = [=](Fn<void()> close) {
  312. if (singlePeer && (*trust)->checked()) {
  313. const auto session = &singlePeer->session();
  314. session->local().markPeerTrustedPayForMessage(
  315. singlePeerId,
  316. singlePeerStars);
  317. }
  318. check();
  319. close();
  320. };
  321. Ui::ConfirmBox(box, {
  322. .text = (singlePeer
  323. ? tr::lng_payment_confirm_text(
  324. tr::now,
  325. lt_count,
  326. stars / messages,
  327. lt_name,
  328. Ui::Text::Bold(singlePeer->shortName()),
  329. Ui::Text::RichLangValue)
  330. : (usersOnly
  331. ? tr::lng_payment_confirm_users
  332. : tr::lng_payment_confirm_chats)(
  333. tr::now,
  334. lt_count,
  335. int(peers.size()),
  336. Ui::Text::RichLangValue)).append(' ').append(
  337. tr::lng_payment_confirm_sure(
  338. tr::now,
  339. lt_count,
  340. messages,
  341. lt_amount,
  342. tr::lng_payment_confirm_amount(
  343. tr::now,
  344. lt_count,
  345. stars,
  346. Ui::Text::RichLangValue),
  347. Ui::Text::RichLangValue)),
  348. .confirmed = proceed,
  349. .confirmText = tr::lng_payment_confirm_button(
  350. lt_count,
  351. rpl::single(messages * 1.)),
  352. .labelStyle = styles.label,
  353. .title = tr::lng_payment_confirm_title(),
  354. });
  355. if (singlePeer) {
  356. const auto skip = st::defaultCheckbox.margin.top();
  357. *trust = box->addRow(
  358. object_ptr<Ui::Checkbox>(
  359. box,
  360. tr::lng_payment_confirm_dont_ask(tr::now),
  361. false,
  362. (styles.checkbox
  363. ? *styles.checkbox
  364. : st::defaultCheckbox)),
  365. st::boxRowPadding + QMargins(0, skip, 0, skip));
  366. }
  367. }));
  368. }
  369. bool SendPaymentHelper::check(
  370. not_null<Window::SessionNavigation*> navigation,
  371. not_null<PeerData*> peer,
  372. int messagesCount,
  373. int starsApproved,
  374. Fn<void(int)> resend,
  375. PaidConfirmStyles styles) {
  376. return check(
  377. navigation->uiShow(),
  378. peer,
  379. messagesCount,
  380. starsApproved,
  381. std::move(resend),
  382. styles);
  383. }
  384. bool SendPaymentHelper::check(
  385. std::shared_ptr<Main::SessionShow> show,
  386. not_null<PeerData*> peer,
  387. int messagesCount,
  388. int starsApproved,
  389. Fn<void(int)> resend,
  390. PaidConfirmStyles styles) {
  391. clear();
  392. const auto details = ComputePaymentDetails(peer, messagesCount);
  393. if (!details) {
  394. _resend = [=] { resend(starsApproved); };
  395. if (!peer->session().credits().loaded()) {
  396. peer->session().credits().loadedValue(
  397. ) | rpl::filter(
  398. rpl::mappers::_1
  399. ) | rpl::take(1) | rpl::start_with_next([=] {
  400. if (const auto callback = base::take(_resend)) {
  401. callback();
  402. }
  403. }, _lifetime);
  404. }
  405. peer->session().changes().peerUpdates(
  406. peer,
  407. Data::PeerUpdate::Flag::FullInfo
  408. ) | rpl::start_with_next([=] {
  409. if (const auto callback = base::take(_resend)) {
  410. callback();
  411. }
  412. }, _lifetime);
  413. return false;
  414. } else if (const auto stars = details->stars; stars > starsApproved) {
  415. ShowSendPaidConfirm(show, peer, *details, [=] {
  416. resend(stars);
  417. }, styles);
  418. return false;
  419. }
  420. return true;
  421. }
  422. void SendPaymentHelper::clear() {
  423. _lifetime.destroy();
  424. _resend = nullptr;
  425. }
  426. void RequestDependentMessageItem(
  427. not_null<HistoryItem*> item,
  428. PeerId peerId,
  429. MsgId msgId) {
  430. if (!IsServerMsgId(msgId)) {
  431. return;
  432. }
  433. const auto fullId = item->fullId();
  434. const auto history = item->history();
  435. const auto session = &history->session();
  436. const auto done = [=] {
  437. if (const auto item = session->data().message(fullId)) {
  438. item->updateDependencyItem();
  439. }
  440. };
  441. history->session().api().requestMessageData(
  442. (peerId ? history->owner().peer(peerId) : history->peer),
  443. msgId,
  444. done);
  445. }
  446. void RequestDependentMessageStory(
  447. not_null<HistoryItem*> item,
  448. PeerId peerId,
  449. StoryId storyId) {
  450. const auto fullId = item->fullId();
  451. const auto history = item->history();
  452. const auto session = &history->session();
  453. const auto done = [=] {
  454. if (const auto item = session->data().message(fullId)) {
  455. item->updateDependencyItem();
  456. }
  457. };
  458. history->owner().stories().resolve(
  459. { peerId ? peerId : history->peer->id, storyId },
  460. done);
  461. }
  462. MessageFlags NewMessageFlags(not_null<PeerData*> peer) {
  463. return MessageFlag::BeingSent
  464. | (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing);
  465. }
  466. TimeId NewMessageDate(TimeId scheduled) {
  467. return scheduled ? scheduled : base::unixtime::now();
  468. }
  469. TimeId NewMessageDate(const Api::SendOptions &options) {
  470. return options.shortcutId ? 1 : NewMessageDate(options.scheduled);
  471. }
  472. PeerId NewMessageFromId(const Api::SendAction &action) {
  473. return action.options.sendAs
  474. ? action.options.sendAs->id
  475. : action.history->peer->amAnonymous()
  476. ? PeerId()
  477. : action.history->session().userPeerId();
  478. }
  479. QString NewMessagePostAuthor(const Api::SendAction &action) {
  480. return !action.history->peer->isBroadcast()
  481. ? QString()
  482. : (action.options.sendAs == action.history->peer)
  483. ? QString()
  484. : action.options.sendAs
  485. ? action.options.sendAs->name()
  486. : action.history->session().user()->name();
  487. }
  488. bool ShouldSendSilent(
  489. not_null<PeerData*> peer,
  490. const Api::SendOptions &options) {
  491. return options.silent
  492. || (peer->isBroadcast()
  493. && peer->owner().notifySettings().silentPosts(peer))
  494. || (peer->session().supportMode()
  495. && peer->session().settings().supportAllSilent());
  496. }
  497. HistoryItem *LookupReplyTo(not_null<History*> history, FullMsgId replyTo) {
  498. return history->owner().message(replyTo);
  499. }
  500. MsgId LookupReplyToTop(not_null<History*> history, HistoryItem *replyTo) {
  501. return (replyTo && replyTo->history() == history)
  502. ? replyTo->replyToTop()
  503. : 0;
  504. }
  505. MsgId LookupReplyToTop(not_null<History*> history, FullReplyTo replyTo) {
  506. return replyTo.topicRootId
  507. ? replyTo.topicRootId
  508. : LookupReplyToTop(
  509. history,
  510. LookupReplyTo(history, replyTo.messageId));
  511. }
  512. bool LookupReplyIsTopicPost(HistoryItem *replyTo) {
  513. return replyTo
  514. && (replyTo->topicRootId() != Data::ForumTopic::kGeneralId);
  515. }
  516. TextWithEntities DropDisallowedCustomEmoji(
  517. not_null<PeerData*> to,
  518. TextWithEntities text) {
  519. if (to->session().premium() || to->isSelf()) {
  520. return text;
  521. }
  522. const auto channel = to->asMegagroup();
  523. const auto allowSetId = channel ? channel->mgInfo->emojiSet.id : 0;
  524. if (!allowSetId) {
  525. text.entities.erase(
  526. ranges::remove(
  527. text.entities,
  528. EntityType::CustomEmoji,
  529. &EntityInText::type),
  530. text.entities.end());
  531. } else {
  532. const auto predicate = [&](const EntityInText &entity) {
  533. if (entity.type() != EntityType::CustomEmoji) {
  534. return false;
  535. }
  536. if (const auto id = Data::ParseCustomEmojiData(entity.data())) {
  537. const auto document = to->owner().document(id);
  538. if (const auto sticker = document->sticker()) {
  539. if (sticker->set.id == allowSetId) {
  540. return false;
  541. }
  542. }
  543. }
  544. return true;
  545. };
  546. text.entities.erase(
  547. ranges::remove_if(text.entities, predicate),
  548. text.entities.end());
  549. }
  550. return text;
  551. }
  552. Main::Session *SessionByUniqueId(uint64 sessionUniqueId) {
  553. if (!sessionUniqueId) {
  554. return nullptr;
  555. }
  556. for (const auto &[index, account] : Core::App().domain().accounts()) {
  557. if (const auto session = account->maybeSession()) {
  558. if (session->uniqueId() == sessionUniqueId) {
  559. return session;
  560. }
  561. }
  562. }
  563. return nullptr;
  564. }
  565. HistoryItem *MessageByGlobalId(GlobalMsgId globalId) {
  566. const auto sessionId = globalId.itemId ? globalId.sessionUniqueId : 0;
  567. if (const auto session = SessionByUniqueId(sessionId)) {
  568. return session->data().message(globalId.itemId);
  569. }
  570. return nullptr;
  571. }
  572. QDateTime ItemDateTime(not_null<const HistoryItem*> item) {
  573. return base::unixtime::parse(item->date());
  574. }
  575. QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline) {
  576. const auto dateText = langDayOfMonthFull(ItemDateTime(item).date());
  577. return !item->isScheduled()
  578. ? dateText
  579. : isUntilOnline
  580. ? tr::lng_scheduled_date_until_online(tr::now)
  581. : tr::lng_scheduled_date(tr::now, lt_date, dateText);
  582. }
  583. bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
  584. return item->isScheduled()
  585. && (item->date() == Api::kScheduledUntilOnlineTimestamp);
  586. }
  587. ClickHandlerPtr JumpToMessageClickHandler(
  588. not_null<HistoryItem*> item,
  589. FullMsgId returnToId,
  590. TextWithEntities highlightPart,
  591. int highlightPartOffsetHint) {
  592. return JumpToMessageClickHandler(
  593. item->history()->peer,
  594. item->id,
  595. returnToId,
  596. std::move(highlightPart),
  597. highlightPartOffsetHint);
  598. }
  599. ClickHandlerPtr JumpToMessageClickHandler(
  600. not_null<PeerData*> peer,
  601. MsgId msgId,
  602. FullMsgId returnToId,
  603. TextWithEntities highlightPart,
  604. int highlightPartOffsetHint) {
  605. return std::make_shared<LambdaClickHandler>([=] {
  606. const auto separate = Core::App().separateWindowFor(peer);
  607. const auto controller = separate
  608. ? separate->sessionController()
  609. : peer->session().tryResolveWindow(peer);
  610. if (controller) {
  611. auto params = Window::SectionShow{
  612. Window::SectionShow::Way::Forward
  613. };
  614. params.highlightPart = highlightPart;
  615. params.highlightPartOffsetHint = highlightPartOffsetHint;
  616. params.origin = Window::SectionShow::OriginMessage{
  617. returnToId
  618. };
  619. if (const auto item = peer->owner().message(peer, msgId)) {
  620. controller->showMessage(item, params);
  621. } else {
  622. controller->showPeerHistory(peer, params, msgId);
  623. }
  624. }
  625. });
  626. }
  627. ClickHandlerPtr JumpToStoryClickHandler(not_null<Data::Story*> story) {
  628. return JumpToStoryClickHandler(story->peer(), story->id());
  629. }
  630. ClickHandlerPtr JumpToStoryClickHandler(
  631. not_null<PeerData*> peer,
  632. StoryId storyId) {
  633. return std::make_shared<LambdaClickHandler>([=] {
  634. const auto separate = Core::App().separateWindowFor(peer);
  635. const auto controller = separate
  636. ? separate->sessionController()
  637. : peer->session().tryResolveWindow();
  638. if (controller) {
  639. controller->openPeerStory(
  640. peer,
  641. storyId,
  642. { Data::StoriesContextSingle() });
  643. }
  644. });
  645. }
  646. ClickHandlerPtr HideSponsoredClickHandler() {
  647. return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
  648. const auto my = context.other.value<ClickHandlerContext>();
  649. if (const auto controller = my.sessionWindow.get()) {
  650. const auto &session = controller->session();
  651. if (session.premium()) {
  652. using Result = Data::SponsoredReportResult;
  653. session.sponsoredMessages().createReportCallback(
  654. my.itemId)(Result::Id("-1"), [](const auto &) {});
  655. } else {
  656. ShowPremiumPreviewBox(controller, PremiumFeature::NoAds);
  657. }
  658. }
  659. });
  660. }
  661. ClickHandlerPtr ReportSponsoredClickHandler(not_null<HistoryItem*> item) {
  662. return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
  663. const auto my = context.other.value<ClickHandlerContext>();
  664. if (const auto controller = my.sessionWindow.get()) {
  665. Menu::ShowSponsored(
  666. controller->widget(),
  667. controller->uiShow(),
  668. item->fullId());
  669. }
  670. });
  671. }
  672. ClickHandlerPtr AboutSponsoredClickHandler() {
  673. return std::make_shared<LambdaClickHandler>([=](ClickContext context) {
  674. const auto my = context.other.value<ClickHandlerContext>();
  675. if (const auto controller = my.sessionWindow.get()) {
  676. Menu::ShowSponsoredAbout(controller->uiShow(), my.itemId);
  677. }
  678. });
  679. }
  680. MessageFlags FlagsFromMTP(
  681. MsgId id,
  682. MTPDmessage::Flags flags,
  683. MessageFlags localFlags) {
  684. using Flag = MessageFlag;
  685. using MTP = MTPDmessage::Flag;
  686. return localFlags
  687. | (IsServerMsgId(id) ? Flag::HistoryEntry : Flag())
  688. | ((flags & MTP::f_out) ? Flag::Outgoing : Flag())
  689. | ((flags & MTP::f_mentioned) ? Flag::MentionsMe : Flag())
  690. | ((flags & MTP::f_media_unread) ? Flag::MediaIsUnread : Flag())
  691. | ((flags & MTP::f_silent) ? Flag::Silent : Flag())
  692. | ((flags & MTP::f_post) ? Flag::Post : Flag())
  693. | ((flags & MTP::f_legacy) ? Flag::Legacy : Flag())
  694. | ((flags & MTP::f_edit_hide) ? Flag::HideEdited : Flag())
  695. | ((flags & MTP::f_pinned) ? Flag::Pinned : Flag())
  696. | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag())
  697. | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag())
  698. | ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag())
  699. | ((flags & MTP::f_quick_reply_shortcut_id)
  700. ? Flag::ShortcutMessage
  701. : Flag())
  702. | ((flags & MTP::f_from_scheduled)
  703. ? Flag::IsOrWasScheduled
  704. : Flag())
  705. | ((flags & MTP::f_views) ? Flag::HasViews : Flag())
  706. | ((flags & MTP::f_noforwards) ? Flag::NoForwards : Flag())
  707. | ((flags & MTP::f_invert_media) ? Flag::InvertMedia : Flag())
  708. | ((flags & MTP::f_video_processing_pending)
  709. ? Flag::EstimatedDate
  710. : Flag());
  711. }
  712. MessageFlags FlagsFromMTP(
  713. MsgId id,
  714. MTPDmessageService::Flags flags,
  715. MessageFlags localFlags) {
  716. using Flag = MessageFlag;
  717. using MTP = MTPDmessageService::Flag;
  718. return localFlags
  719. | (IsServerMsgId(id) ? Flag::HistoryEntry : Flag())
  720. | ((flags & MTP::f_out) ? Flag::Outgoing : Flag())
  721. | ((flags & MTP::f_mentioned) ? Flag::MentionsMe : Flag())
  722. | ((flags & MTP::f_media_unread) ? Flag::MediaIsUnread : Flag())
  723. | ((flags & MTP::f_silent) ? Flag::Silent : Flag())
  724. | ((flags & MTP::f_post) ? Flag::Post : Flag())
  725. | ((flags & MTP::f_legacy) ? Flag::Legacy : Flag())
  726. | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag())
  727. | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag())
  728. | ((flags & MTP::f_reactions_are_possible)
  729. ? Flag::ReactionsAllowed
  730. : Flag());
  731. }
  732. MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) {
  733. if (const auto replyTo = action.replyTo) {
  734. if (replyTo.storyId) {
  735. return MTP_messageReplyStoryHeader(
  736. peerToMTP(replyTo.storyId.peer),
  737. MTP_int(replyTo.storyId.story));
  738. }
  739. using Flag = MTPDmessageReplyHeader::Flag;
  740. const auto historyPeer = action.history->peer->id;
  741. const auto externalPeerId = (replyTo.messageId.peer == historyPeer)
  742. ? PeerId()
  743. : replyTo.messageId.peer;
  744. const auto replyToTop = LookupReplyToTop(action.history, replyTo);
  745. auto quoteEntities = Api::EntitiesToMTP(
  746. &action.history->session(),
  747. replyTo.quote.entities,
  748. Api::ConvertOption::SkipLocal);
  749. return MTP_messageReplyHeader(
  750. MTP_flags(Flag::f_reply_to_msg_id
  751. | (replyToTop ? Flag::f_reply_to_top_id : Flag())
  752. | (externalPeerId ? Flag::f_reply_to_peer_id : Flag())
  753. | (replyTo.quote.empty()
  754. ? Flag()
  755. : (Flag::f_quote
  756. | Flag::f_quote_text
  757. | Flag::f_quote_offset))
  758. | (quoteEntities.v.empty()
  759. ? Flag()
  760. : Flag::f_quote_entities)),
  761. MTP_int(replyTo.messageId.msg),
  762. peerToMTP(externalPeerId),
  763. MTPMessageFwdHeader(), // reply_from
  764. MTPMessageMedia(), // reply_media
  765. MTP_int(replyToTop),
  766. MTP_string(replyTo.quote.text),
  767. quoteEntities,
  768. MTP_int(replyTo.quoteOffset));
  769. }
  770. return MTPMessageReplyHeader();
  771. }
  772. MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) {
  773. using Result = MediaCheckResult;
  774. return media.match([](const MTPDmessageMediaEmpty &) {
  775. return Result::Good;
  776. }, [](const MTPDmessageMediaContact &) {
  777. return Result::Good;
  778. }, [](const MTPDmessageMediaGeo &data) {
  779. return data.vgeo().match([](const MTPDgeoPoint &) {
  780. return Result::Good;
  781. }, [](const MTPDgeoPointEmpty &) {
  782. return Result::Empty;
  783. });
  784. }, [](const MTPDmessageMediaVenue &data) {
  785. return data.vgeo().match([](const MTPDgeoPoint &) {
  786. return Result::Good;
  787. }, [](const MTPDgeoPointEmpty &) {
  788. return Result::Empty;
  789. });
  790. }, [](const MTPDmessageMediaGeoLive &data) {
  791. return data.vgeo().match([](const MTPDgeoPoint &) {
  792. return Result::Good;
  793. }, [](const MTPDgeoPointEmpty &) {
  794. return Result::Empty;
  795. });
  796. }, [](const MTPDmessageMediaPhoto &data) {
  797. const auto photo = data.vphoto();
  798. if (data.vttl_seconds()) {
  799. return Result::HasUnsupportedTimeToLive;
  800. } else if (!photo) {
  801. return Result::Empty;
  802. }
  803. return photo->match([](const MTPDphoto &) {
  804. return Result::Good;
  805. }, [](const MTPDphotoEmpty &) {
  806. return Result::Empty;
  807. });
  808. }, [](const MTPDmessageMediaDocument &data) {
  809. const auto document = data.vdocument();
  810. if (data.vttl_seconds()) {
  811. if (data.is_video()) {
  812. return Result::HasUnsupportedTimeToLive;
  813. } else if (!document) {
  814. return Result::HasExpiredMediaTimeToLive;
  815. }
  816. } else if (!document) {
  817. return Result::Empty;
  818. }
  819. return document->match([](const MTPDdocument &) {
  820. return Result::Good;
  821. }, [](const MTPDdocumentEmpty &) {
  822. return Result::Empty;
  823. });
  824. }, [](const MTPDmessageMediaWebPage &data) {
  825. return data.vwebpage().match([](const MTPDwebPage &) {
  826. return Result::Good;
  827. }, [](const MTPDwebPageEmpty &) {
  828. return Result::Good;
  829. }, [](const MTPDwebPagePending &) {
  830. return Result::Good;
  831. }, [](const MTPDwebPageNotModified &) {
  832. return Result::Unsupported;
  833. });
  834. }, [](const MTPDmessageMediaGame &data) {
  835. return data.vgame().match([](const MTPDgame &) {
  836. return Result::Good;
  837. });
  838. }, [](const MTPDmessageMediaInvoice &) {
  839. return Result::Good;
  840. }, [](const MTPDmessageMediaPoll &) {
  841. return Result::Good;
  842. }, [](const MTPDmessageMediaDice &) {
  843. return Result::Good;
  844. }, [](const MTPDmessageMediaStory &data) {
  845. return data.is_via_mention()
  846. ? Result::HasStoryMention
  847. : Result::Good;
  848. }, [](const MTPDmessageMediaGiveaway &) {
  849. return Result::Good;
  850. }, [](const MTPDmessageMediaGiveawayResults &) {
  851. return Result::Good;
  852. }, [](const MTPDmessageMediaPaidMedia &) {
  853. return Result::Good;
  854. }, [](const MTPDmessageMediaUnsupported &) {
  855. return Result::Unsupported;
  856. });
  857. }
  858. [[nodiscard]] CallId CallIdFromInput(const MTPInputGroupCall &data) {
  859. return data.match([&](const MTPDinputGroupCall &data) {
  860. return data.vid().v;
  861. });
  862. }
  863. std::vector<not_null<UserData*>> ParseInvitedToCallUsers(
  864. not_null<HistoryItem*> item,
  865. const QVector<MTPlong> &users) {
  866. auto &owner = item->history()->owner();
  867. return ranges::views::all(
  868. users
  869. ) | ranges::views::transform([&](const MTPlong &id) {
  870. return owner.user(id.v);
  871. }) | ranges::to_vector;
  872. }
  873. PreparedServiceText GenerateJoinedText(
  874. not_null<History*> history,
  875. not_null<UserData*> inviter,
  876. bool viaRequest) {
  877. if (inviter->id != history->session().userPeerId()) {
  878. auto result = PreparedServiceText();
  879. result.links.push_back(inviter->createOpenLink());
  880. result.text = (history->peer->isMegagroup()
  881. ? tr::lng_action_add_you_group
  882. : tr::lng_action_add_you)(
  883. tr::now,
  884. lt_from,
  885. Ui::Text::Link(inviter->name(), QString()),
  886. Ui::Text::WithEntities);
  887. return result;
  888. } else if (history->peer->isMegagroup()) {
  889. if (viaRequest) {
  890. return { tr::lng_action_you_joined_by_request(
  891. tr::now,
  892. Ui::Text::WithEntities) };
  893. }
  894. auto self = history->session().user();
  895. auto result = PreparedServiceText();
  896. result.links.push_back(self->createOpenLink());
  897. result.text = tr::lng_action_user_joined(
  898. tr::now,
  899. lt_from,
  900. Ui::Text::Link(self->name(), QString()),
  901. Ui::Text::WithEntities);
  902. return result;
  903. }
  904. return { viaRequest
  905. ? tr::lng_action_you_joined_by_request_channel(
  906. tr::now,
  907. Ui::Text::WithEntities)
  908. : tr::lng_action_you_joined(tr::now, Ui::Text::WithEntities) };
  909. }
  910. not_null<HistoryItem*> GenerateJoinedMessage(
  911. not_null<History*> history,
  912. TimeId inviteDate,
  913. not_null<UserData*> inviter,
  914. bool viaRequest) {
  915. return history->makeMessage({
  916. .id = history->owner().nextLocalMessageId(),
  917. .flags = MessageFlag::Local | MessageFlag::ShowSimilarChannels,
  918. .date = inviteDate,
  919. }, GenerateJoinedText(history, inviter, viaRequest));
  920. }
  921. std::optional<bool> PeerHasThisCall(
  922. not_null<PeerData*> peer,
  923. CallId id) {
  924. const auto call = peer->groupCall();
  925. return call
  926. ? std::make_optional(call->id() == id)
  927. : PeerCallKnown(peer)
  928. ? std::make_optional(false)
  929. : std::nullopt;
  930. }
  931. [[nodiscard]] rpl::producer<bool> PeerHasThisCallValue(
  932. not_null<PeerData*> peer,
  933. CallId id) {
  934. return peer->session().changes().peerFlagsValue(
  935. peer,
  936. Data::PeerUpdate::Flag::GroupCall
  937. ) | rpl::filter([=] {
  938. return PeerCallKnown(peer);
  939. }) | rpl::map([=] {
  940. const auto call = peer->groupCall();
  941. return (call && call->id() == id);
  942. }) | rpl::distinct_until_changed(
  943. ) | rpl::take_while([=](bool hasThisCall) {
  944. return hasThisCall;
  945. }) | rpl::then(
  946. rpl::single(false)
  947. );
  948. }
  949. [[nodiscard]] ClickHandlerPtr GroupCallClickHandler(
  950. not_null<PeerData*> peer,
  951. CallId callId) {
  952. return std::make_shared<LambdaClickHandler>([=] {
  953. const auto call = peer->groupCall();
  954. if (call && call->id() == callId) {
  955. const auto &windows = peer->session().windows();
  956. if (windows.empty()) {
  957. Core::App().domain().activate(&peer->session().account());
  958. if (windows.empty()) {
  959. return;
  960. }
  961. }
  962. windows.front()->startOrJoinGroupCall(peer, {});
  963. }
  964. });
  965. }
  966. [[nodiscard]] MessageFlags FinalizeMessageFlags(
  967. not_null<History*> history,
  968. MessageFlags flags) {
  969. if (!(flags & MessageFlag::FakeHistoryItem)
  970. && !(flags & MessageFlag::IsOrWasScheduled)
  971. && !(flags & MessageFlag::ShortcutMessage)
  972. && !(flags & MessageFlag::AdminLogEntry)) {
  973. flags |= MessageFlag::HistoryEntry;
  974. if (history->peer->isSelf()) {
  975. flags |= MessageFlag::ReactionsAreTags;
  976. }
  977. }
  978. return flags;
  979. }
  980. using OnStackUsers = std::array<UserData*, kMaxUnreadReactions>;
  981. [[nodiscard]] OnStackUsers LookupRecentUnreadReactedUsers(
  982. not_null<HistoryItem*> item) {
  983. auto result = OnStackUsers();
  984. auto index = 0;
  985. for (const auto &[emoji, reactions] : item->recentReactions()) {
  986. for (const auto &reaction : reactions) {
  987. if (!reaction.unread) {
  988. continue;
  989. }
  990. if (const auto user = reaction.peer->asUser()) {
  991. result[index++] = user;
  992. if (index == result.size()) {
  993. return result;
  994. }
  995. }
  996. }
  997. }
  998. return result;
  999. }
  1000. void CheckReactionNotificationSchedule(
  1001. not_null<HistoryItem*> item,
  1002. const OnStackUsers &wasUsers) {
  1003. // Call to addToUnreadThings may have read the reaction already.
  1004. if (!item->hasUnreadReaction()) {
  1005. return;
  1006. }
  1007. for (const auto &[emoji, reactions] : item->recentReactions()) {
  1008. for (const auto &reaction : reactions) {
  1009. if (!reaction.unread) {
  1010. continue;
  1011. }
  1012. const auto user = reaction.peer->asUser();
  1013. if (!user
  1014. || !user->isContact()
  1015. || ranges::contains(wasUsers, user)) {
  1016. continue;
  1017. }
  1018. using Status = PeerData::BlockStatus;
  1019. if (user->blockStatus() == Status::Unknown) {
  1020. user->updateFull();
  1021. }
  1022. const auto notification = Data::ItemNotification{
  1023. .item = item,
  1024. .reactionSender = user,
  1025. .type = Data::ItemNotificationType::Reaction,
  1026. };
  1027. item->notificationThread()->pushNotification(notification);
  1028. Core::App().notifications().schedule(notification);
  1029. return;
  1030. }
  1031. }
  1032. }
  1033. [[nodiscard]] MessageFlags NewForwardedFlags(
  1034. not_null<PeerData*> peer,
  1035. PeerId from,
  1036. not_null<HistoryItem*> fwd) {
  1037. auto result = NewMessageFlags(peer);
  1038. if (from) {
  1039. result |= MessageFlag::HasFromId;
  1040. }
  1041. if (const auto media = fwd->media()) {
  1042. if ((!peer->isChannel() || peer->isMegagroup())
  1043. && media->forwardedBecomesUnread()) {
  1044. result |= MessageFlag::MediaIsUnread;
  1045. }
  1046. }
  1047. if (fwd->hasViews()) {
  1048. result |= MessageFlag::HasViews;
  1049. }
  1050. return result;
  1051. }
  1052. [[nodiscard]] bool CopyMarkupToForward(not_null<const HistoryItem*> item) {
  1053. auto mediaOriginal = item->media();
  1054. if (mediaOriginal && mediaOriginal->game()) {
  1055. // Copy inline keyboard when forwarding messages with a game.
  1056. return true;
  1057. }
  1058. const auto markup = item->inlineReplyMarkup();
  1059. if (!markup) {
  1060. return false;
  1061. }
  1062. using Type = HistoryMessageMarkupButton::Type;
  1063. for (const auto &row : markup->data.rows) {
  1064. for (const auto &button : row) {
  1065. const auto switchInline = (button.type == Type::SwitchInline)
  1066. || (button.type == Type::SwitchInlineSame);
  1067. const auto url = (button.type == Type::Url)
  1068. || (button.type == Type::Auth);
  1069. if ((!switchInline || !item->viaBot()) && !url) {
  1070. return false;
  1071. }
  1072. }
  1073. }
  1074. return true;
  1075. }
  1076. [[nodiscard]] TextWithEntities EnsureNonEmpty(
  1077. const TextWithEntities &text) {
  1078. return !text.text.isEmpty() ? text : TextWithEntities{ u":-("_q };
  1079. }
  1080. [[nodiscard]] TextWithEntities UnsupportedMessageText() {
  1081. const auto siteLink = u"https://desktop.telegram.org"_q;
  1082. auto result = TextWithEntities{
  1083. tr::lng_message_unsupported(tr::now, lt_link, siteLink)
  1084. };
  1085. TextUtilities::ParseEntities(result, Ui::ItemTextNoMonoOptions().flags);
  1086. result.entities.push_front(
  1087. EntityInText(EntityType::Italic, 0, result.text.size()));
  1088. return result;
  1089. }
  1090. void ShowTrialTranscribesToast(int left, TimeId until) {
  1091. const auto window = Core::App().activeWindow();
  1092. if (!window) {
  1093. return;
  1094. }
  1095. const auto filter = [=](const auto &...) {
  1096. if (const auto controller = window->sessionController()) {
  1097. ShowPremiumPreviewBox(controller, PremiumFeature::VoiceToText);
  1098. window->activate();
  1099. }
  1100. return false;
  1101. };
  1102. const auto date = langDateTime(base::unixtime::parse(until));
  1103. constexpr auto kToastDuration = crl::time(4000);
  1104. const auto text = left
  1105. ? tr::lng_audio_transcribe_trials_left(
  1106. tr::now,
  1107. lt_count,
  1108. left,
  1109. lt_date,
  1110. { date },
  1111. Ui::Text::WithEntities)
  1112. : tr::lng_audio_transcribe_trials_over(
  1113. tr::now,
  1114. lt_date,
  1115. Ui::Text::Bold(date),
  1116. lt_link,
  1117. Ui::Text::Link(tr::lng_settings_privacy_premium_link(tr::now)),
  1118. Ui::Text::WithEntities);
  1119. window->uiShow()->showToast(Ui::Toast::Config{
  1120. .text = text,
  1121. .filter = filter,
  1122. .duration = kToastDuration,
  1123. });
  1124. }
  1125. void ClearMediaAsExpired(not_null<HistoryItem*> item) {
  1126. if (const auto media = item->media()) {
  1127. if (!media->ttlSeconds()) {
  1128. return;
  1129. }
  1130. if (const auto document = media->document()) {
  1131. item->applyEditionToHistoryCleared();
  1132. auto text = (document->isVideoFile()
  1133. ? tr::lng_ttl_video_expired
  1134. : document->isVoiceMessage()
  1135. ? tr::lng_ttl_voice_expired
  1136. : document->isVideoMessage()
  1137. ? tr::lng_ttl_round_expired
  1138. : tr::lng_message_empty)(tr::now, Ui::Text::WithEntities);
  1139. item->updateServiceText(PreparedServiceText{ std::move(text) });
  1140. } else if (const auto photo = media->photo()) {
  1141. item->applyEditionToHistoryCleared();
  1142. item->updateServiceText(PreparedServiceText{
  1143. tr::lng_ttl_photo_expired(tr::now, Ui::Text::WithEntities)
  1144. });
  1145. }
  1146. }
  1147. }
  1148. int ItemsForwardSendersCount(const HistoryItemsList &list) {
  1149. auto peers = base::flat_set<not_null<PeerData*>>();
  1150. auto names = base::flat_set<QString>();
  1151. for (const auto &item : list) {
  1152. if (const auto peer = item->originalSender()) {
  1153. peers.emplace(peer);
  1154. } else {
  1155. names.emplace(item->originalHiddenSenderInfo()->name);
  1156. }
  1157. }
  1158. return int(peers.size()) + int(names.size());
  1159. }
  1160. int ItemsForwardCaptionsCount(const HistoryItemsList &list) {
  1161. auto result = 0;
  1162. for (const auto &item : list) {
  1163. if (const auto media = item->media()) {
  1164. if (!item->originalText().text.isEmpty()
  1165. && media->allowsEditCaption()) {
  1166. ++result;
  1167. }
  1168. }
  1169. }
  1170. return result;
  1171. }