history_view_reply.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  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/view/history_view_reply.h"
  8. #include "core/click_handler_types.h"
  9. #include "core/ui_integration.h"
  10. #include "data/stickers/data_custom_emoji.h"
  11. #include "data/data_channel.h"
  12. #include "data/data_peer.h"
  13. #include "data/data_session.h"
  14. #include "data/data_story.h"
  15. #include "data/data_user.h"
  16. #include "history/view/history_view_item_preview.h"
  17. #include "history/history.h"
  18. #include "history/history_item.h"
  19. #include "history/history_item_components.h"
  20. #include "history/history_item_helpers.h"
  21. #include "lang/lang_keys.h"
  22. #include "main/main_session.h"
  23. #include "ui/chat/chat_style.h"
  24. #include "ui/effects/ripple_animation.h"
  25. #include "ui/effects/spoiler_mess.h"
  26. #include "ui/text/text_options.h"
  27. #include "ui/text/text_utilities.h"
  28. #include "ui/painter.h"
  29. #include "ui/power_saving.h"
  30. #include "window/window_session_controller.h"
  31. #include "styles/style_chat.h"
  32. #include "styles/style_dialogs.h"
  33. namespace HistoryView {
  34. namespace {
  35. constexpr auto kNonExpandedLinesLimit = 5;
  36. } // namespace
  37. void ValidateBackgroundEmoji(
  38. DocumentId backgroundEmojiId,
  39. not_null<Ui::BackgroundEmojiData*> data,
  40. not_null<Ui::BackgroundEmojiCache*> cache,
  41. not_null<Ui::Text::QuotePaintCache*> quote,
  42. not_null<const Element*> view) {
  43. if (data->firstFrameMask.isNull() && !data->emoji) {
  44. data->emoji = CreateBackgroundEmojiInstance(
  45. &view->history()->owner(),
  46. backgroundEmojiId,
  47. crl::guard(view, [=] { view->repaint(); }));
  48. }
  49. ValidateBackgroundEmoji(backgroundEmojiId, data, cache, quote);
  50. }
  51. void ValidateBackgroundEmoji(
  52. DocumentId backgroundEmojiId,
  53. not_null<Ui::BackgroundEmojiData*> data,
  54. not_null<Ui::BackgroundEmojiCache*> cache,
  55. not_null<Ui::Text::QuotePaintCache*> quote) {
  56. Expects(!data->firstFrameMask.isNull() || data->emoji != nullptr);
  57. if (data->firstFrameMask.isNull()) {
  58. if (!cache->frames[0].isNull()) {
  59. for (auto &frame : cache->frames) {
  60. frame = QImage();
  61. }
  62. }
  63. if (!data->emoji->ready()) {
  64. return;
  65. }
  66. const auto tag = Data::CustomEmojiSizeTag::Isolated;
  67. const auto size = Data::FrameSizeFromTag(tag);
  68. data->firstFrameMask = QImage(
  69. QSize(size, size),
  70. QImage::Format_ARGB32_Premultiplied);
  71. data->firstFrameMask.fill(Qt::transparent);
  72. data->firstFrameMask.setDevicePixelRatio(style::DevicePixelRatio());
  73. auto p = Painter(&data->firstFrameMask);
  74. data->emoji->paint(p, {
  75. .textColor = QColor(255, 255, 255),
  76. .position = QPoint(0, 0),
  77. .internal = {
  78. .forceFirstFrame = true,
  79. },
  80. });
  81. p.end();
  82. data->emoji = nullptr;
  83. }
  84. if (!cache->frames[0].isNull() && cache->color == quote->icon) {
  85. return;
  86. }
  87. cache->color = quote->icon;
  88. const auto ratio = style::DevicePixelRatio();
  89. auto colorized = QImage(
  90. data->firstFrameMask.size(),
  91. QImage::Format_ARGB32_Premultiplied);
  92. colorized.setDevicePixelRatio(ratio);
  93. style::colorizeImage(
  94. data->firstFrameMask,
  95. cache->color,
  96. &colorized,
  97. QRect(), // src
  98. QPoint(), // dst
  99. true); // use alpha
  100. const auto make = [&](int size) {
  101. size = style::ConvertScale(size) * ratio;
  102. auto result = colorized.scaled(
  103. size,
  104. size,
  105. Qt::IgnoreAspectRatio,
  106. Qt::SmoothTransformation);
  107. result.setDevicePixelRatio(ratio);
  108. return result;
  109. };
  110. constexpr auto kSize1 = 12;
  111. constexpr auto kSize2 = 16;
  112. constexpr auto kSize3 = 20;
  113. cache->frames[0] = make(kSize1);
  114. cache->frames[1] = make(kSize2);
  115. cache->frames[2] = make(kSize3);
  116. }
  117. auto CreateBackgroundEmojiInstance(
  118. not_null<Data::Session*> owner,
  119. DocumentId backgroundEmojiId,
  120. Fn<void()> repaint)
  121. -> std::unique_ptr<Ui::Text::CustomEmoji> {
  122. return owner->customEmojiManager().create(
  123. backgroundEmojiId,
  124. repaint,
  125. Data::CustomEmojiSizeTag::Isolated);
  126. }
  127. void FillBackgroundEmoji(
  128. QPainter &p,
  129. const QRect &rect,
  130. bool quote,
  131. const Ui::BackgroundEmojiCache &cache) {
  132. p.setClipRect(rect);
  133. const auto &frames = cache.frames;
  134. const auto right = rect.x() + rect.width();
  135. const auto paint = [&](int x, int y, int index, float64 opacity) {
  136. y = style::ConvertScale(y);
  137. if (y >= rect.height()) {
  138. return;
  139. }
  140. p.setOpacity(opacity);
  141. p.drawImage(
  142. right - style::ConvertScale(x + (quote ? 12 : 0)),
  143. rect.y() + y,
  144. frames[index]);
  145. };
  146. paint(28, 4, 2, 0.32);
  147. paint(51, 15, 1, 0.32);
  148. paint(64, -2, 0, 0.28);
  149. paint(87, 11, 1, 0.24);
  150. paint(125, -2, 2, 0.16);
  151. paint(28, 31, 1, 0.24);
  152. paint(72, 33, 2, 0.2);
  153. paint(46, 52, 1, 0.24);
  154. paint(24, 55, 2, 0.18);
  155. if (quote) {
  156. paint(4, 23, 1, 0.28);
  157. paint(0, 48, 0, 0.24);
  158. }
  159. p.setClipping(false);
  160. p.setOpacity(1.);
  161. }
  162. Reply::Reply()
  163. : _name(st::maxSignatureSize / 2)
  164. , _text(st::maxSignatureSize / 2) {
  165. }
  166. Reply &Reply::operator=(Reply &&other) = default;
  167. Reply::~Reply() = default;
  168. void Reply::update(
  169. not_null<Element*> view,
  170. not_null<HistoryMessageReply*> data) {
  171. const auto item = view->data();
  172. const auto &fields = data->fields();
  173. const auto message = data->resolvedMessage.get();
  174. const auto story = data->resolvedStory.get();
  175. const auto externalMedia = fields.externalMedia.get();
  176. if (!_externalSender) {
  177. if (const auto id = fields.externalSenderId) {
  178. _externalSender = view->history()->owner().peer(id);
  179. }
  180. }
  181. _colorPeer = message
  182. ? message->contentColorsFrom()
  183. : story
  184. ? story->peer().get()
  185. : _externalSender
  186. ? _externalSender
  187. : nullptr;
  188. _hiddenSenderColorIndexPlusOne = (!_colorPeer && message)
  189. ? (message->originalHiddenSenderInfo()->colorIndex + 1)
  190. : 0;
  191. const auto hasPreview = (story && story->hasReplyPreview())
  192. || (message
  193. && message->media()
  194. && message->media()->hasReplyPreview())
  195. || (externalMedia && externalMedia->hasReplyPreview());
  196. _hasPreview = hasPreview ? 1 : 0;
  197. _displaying = data->displaying() ? 1 : 0;
  198. _multiline = data->multiline() ? 1 : 0;
  199. _replyToStory = (fields.storyId != 0);
  200. const auto hasQuoteIcon = _displaying
  201. && fields.manualQuote
  202. && !fields.quote.empty();
  203. _hasQuoteIcon = hasQuoteIcon ? 1 : 0;
  204. const auto text = (!_displaying && data->unavailable())
  205. ? TextWithEntities()
  206. : (message && (fields.quote.empty() || !fields.manualQuote))
  207. ? message->inReplyText()
  208. : !fields.quote.empty()
  209. ? fields.quote
  210. : story
  211. ? story->inReplyText()
  212. : externalMedia
  213. ? externalMedia->toPreview({
  214. .hideSender = true,
  215. .hideCaption = true,
  216. .ignoreMessageText = true,
  217. .generateImages = false,
  218. .ignoreGroup = true,
  219. .ignoreTopic = true,
  220. }).text
  221. : TextWithEntities();
  222. const auto repaint = [=] { item->customEmojiRepaint(); };
  223. const auto context = Core::TextContext({
  224. .session = &view->history()->session(),
  225. .repaint = repaint,
  226. });
  227. _text.setMarkedText(
  228. st::defaultTextStyle,
  229. text,
  230. _multiline ? Ui::ItemTextDefaultOptions() : Ui::DialogTextOptions(),
  231. context);
  232. updateName(view, data);
  233. if (_displaying) {
  234. setLinkFrom(view, data);
  235. const auto media = message ? message->media() : nullptr;
  236. if (!media || !media->hasReplyPreview() || !media->hasSpoiler()) {
  237. _spoiler = nullptr;
  238. } else if (!_spoiler) {
  239. _spoiler = std::make_unique<Ui::SpoilerAnimation>(repaint);
  240. }
  241. } else {
  242. _spoiler = nullptr;
  243. }
  244. }
  245. bool Reply::expand() {
  246. if (!_expandable || _expanded) {
  247. return false;
  248. }
  249. _expanded = true;
  250. return true;
  251. }
  252. void Reply::setLinkFrom(
  253. not_null<Element*> view,
  254. not_null<HistoryMessageReply*> data) {
  255. const auto weak = base::make_weak(view);
  256. const auto &fields = data->fields();
  257. const auto externalChannelId = peerToChannel(fields.externalPeerId);
  258. const auto messageId = fields.messageId;
  259. const auto quote = fields.manualQuote
  260. ? fields.quote
  261. : TextWithEntities();
  262. const auto quoteOffset = fields.quoteOffset;
  263. const auto returnToId = view->data()->fullId();
  264. const auto externalLink = [=](ClickContext context) {
  265. const auto my = context.other.value<ClickHandlerContext>();
  266. if (const auto controller = my.sessionWindow.get()) {
  267. auto error = QString();
  268. const auto owner = &controller->session().data();
  269. if (const auto view = weak.get()) {
  270. if (const auto reply = view->Get<Reply>()) {
  271. if (reply->expand()) {
  272. owner->requestViewResize(view);
  273. return;
  274. }
  275. }
  276. }
  277. if (externalChannelId) {
  278. const auto channel = owner->channel(externalChannelId);
  279. if (!channel->isForbidden()) {
  280. if (messageId) {
  281. JumpToMessageClickHandler(
  282. channel,
  283. messageId,
  284. returnToId,
  285. quote,
  286. quoteOffset
  287. )->onClick(context);
  288. } else {
  289. controller->showPeerInfo(channel);
  290. }
  291. } else if (channel->isBroadcast()) {
  292. error = tr::lng_channel_not_accessible(tr::now);
  293. } else {
  294. error = tr::lng_group_not_accessible(tr::now);
  295. }
  296. } else {
  297. error = tr::lng_reply_from_private_chat(tr::now);
  298. }
  299. if (!error.isEmpty()) {
  300. controller->showToast(error);
  301. }
  302. }
  303. };
  304. const auto message = data->resolvedMessage.get();
  305. const auto story = data->resolvedStory.get();
  306. _link = message
  307. ? JumpToMessageClickHandler(message, returnToId, quote, quoteOffset)
  308. : story
  309. ? JumpToStoryClickHandler(story)
  310. : (data->external()
  311. && (!fields.messageId
  312. || (data->unavailable() && externalChannelId)))
  313. ? std::make_shared<LambdaClickHandler>(externalLink)
  314. : nullptr;
  315. }
  316. PeerData *Reply::sender(
  317. not_null<const Element*> view,
  318. not_null<HistoryMessageReply*> data) const {
  319. const auto message = data->resolvedMessage.get();
  320. if (const auto story = data->resolvedStory.get()) {
  321. return story->peer();
  322. } else if (!message) {
  323. return _externalSender;
  324. } else if (view->data()->Has<HistoryMessageForwarded>()) {
  325. // Forward of a reply. Show reply-to original sender.
  326. const auto forwarded = message->Get<HistoryMessageForwarded>();
  327. if (forwarded) {
  328. return forwarded->originalSender;
  329. }
  330. }
  331. if (const auto from = message->displayFrom()) {
  332. return from;
  333. }
  334. return message->author().get();
  335. }
  336. QString Reply::senderName(
  337. not_null<const Element*> view,
  338. not_null<HistoryMessageReply*> data,
  339. bool shorten) const {
  340. if (const auto peer = sender(view, data)) {
  341. return senderName(peer, shorten);
  342. } else if (!data->resolvedMessage) {
  343. return data->fields().externalSenderName;
  344. } else if (view->data()->Has<HistoryMessageForwarded>()) {
  345. // Forward of a reply. Show reply-to original sender.
  346. const auto forwarded
  347. = data->resolvedMessage->Get<HistoryMessageForwarded>();
  348. if (forwarded) {
  349. Assert(forwarded->originalHiddenSenderInfo != nullptr);
  350. return forwarded->originalHiddenSenderInfo->name;
  351. }
  352. }
  353. return QString();
  354. }
  355. QString Reply::senderName(
  356. not_null<PeerData*> peer,
  357. bool shorten) const {
  358. const auto user = shorten ? peer->asUser() : nullptr;
  359. return user ? user->firstName : peer->name();
  360. }
  361. bool Reply::isNameUpdated(
  362. not_null<const Element*> view,
  363. not_null<HistoryMessageReply*> data) const {
  364. if (const auto from = sender(view, data)) {
  365. if (_nameVersion < from->nameVersion()) {
  366. updateName(view, data, from);
  367. return true;
  368. }
  369. }
  370. return false;
  371. }
  372. void Reply::updateName(
  373. not_null<const Element*> view,
  374. not_null<HistoryMessageReply*> data,
  375. std::optional<PeerData*> resolvedSender) const {
  376. auto viaBotUsername = QString();
  377. const auto message = data->resolvedMessage.get();
  378. const auto forwarded = message
  379. ? message->Get<HistoryMessageForwarded>()
  380. : nullptr;
  381. if (message && !forwarded) {
  382. if (const auto bot = message->viaBot()) {
  383. viaBotUsername = bot->username();
  384. }
  385. }
  386. const auto history = view->history();
  387. const auto &fields = data->fields();
  388. const auto sender = resolvedSender.value_or(this->sender(view, data));
  389. const auto externalPeer = fields.externalPeerId
  390. ? history->owner().peer(fields.externalPeerId).get()
  391. : nullptr;
  392. const auto displayAsExternal = data->displayAsExternal(view->data());
  393. const auto groupNameAdded = displayAsExternal
  394. && externalPeer
  395. && (externalPeer != sender)
  396. && (externalPeer->isChat() || externalPeer->isMegagroup());
  397. const auto originalNameAdded = !displayAsExternal
  398. && forwarded
  399. && !message->isDiscussionPost()
  400. && (forwarded->forwardOfForward()
  401. || (!message->showForwardsFromSender(forwarded)
  402. && !view->data()->Has<HistoryMessageForwarded>()));
  403. const auto shorten = !viaBotUsername.isEmpty()
  404. || groupNameAdded
  405. || originalNameAdded;
  406. const auto name = sender
  407. ? senderName(sender, shorten)
  408. : senderName(view, data, shorten);
  409. const auto previewSkip = _hasPreview
  410. ? (st::messageQuoteStyle.outline
  411. + st::historyReplyPreviewMargin.left()
  412. + st::historyReplyPreview
  413. + st::historyReplyPreviewMargin.right()
  414. - st::historyReplyPadding.left())
  415. : 0;
  416. auto nameFull = TextWithEntities();
  417. if (displayAsExternal && !groupNameAdded && !fields.storyId) {
  418. nameFull.append(PeerEmoji(history, sender));
  419. }
  420. nameFull.append(name);
  421. if (groupNameAdded) {
  422. nameFull.append(' ').append(PeerEmoji(history, externalPeer));
  423. nameFull.append(externalPeer->name());
  424. } else if (originalNameAdded) {
  425. nameFull.append(' ').append(ForwardEmoji(&history->owner()));
  426. nameFull.append(forwarded->originalSender
  427. ? forwarded->originalSender->name()
  428. : forwarded->originalHiddenSenderInfo->name);
  429. }
  430. if (!viaBotUsername.isEmpty()) {
  431. nameFull.append(u" @"_q).append(viaBotUsername);
  432. }
  433. const auto context = Core::TextContext({
  434. .session = &history->session(),
  435. .customEmojiLoopLimit = 1,
  436. });
  437. _name.setMarkedText(
  438. st::fwdTextStyle,
  439. nameFull,
  440. Ui::NameTextOptions(),
  441. context);
  442. if (sender) {
  443. _nameVersion = sender->nameVersion();
  444. }
  445. const auto nameMaxWidth = previewSkip
  446. + _name.maxWidth()
  447. + (_hasQuoteIcon
  448. ? st::messageTextStyle.blockquote.icon.width()
  449. : 0);
  450. const auto storySkip = fields.storyId
  451. ? (st::dialogsMiniReplyStory.skipText
  452. + st::dialogsMiniReplyStory.icon.icon.width())
  453. : 0;
  454. const auto optimalTextSize = _multiline
  455. ? countMultilineOptimalSize(previewSkip)
  456. : QSize(
  457. (previewSkip
  458. + storySkip
  459. + std::min(_text.maxWidth(), st::maxSignatureSize)),
  460. st::normalFont->height);
  461. _maxWidth = std::max(nameMaxWidth, optimalTextSize.width());
  462. if (!data->displaying()) {
  463. const auto unavailable = data->unavailable();
  464. _stateText = ((fields.messageId || fields.storyId) && !unavailable)
  465. ? tr::lng_profile_loading(tr::now)
  466. : fields.storyId
  467. ? tr::lng_deleted_story(tr::now)
  468. : tr::lng_deleted_message(tr::now);
  469. const auto phraseWidth = st::msgDateFont->width(_stateText);
  470. _maxWidth = unavailable
  471. ? phraseWidth
  472. : std::max(_maxWidth, phraseWidth);
  473. } else {
  474. _stateText = QString();
  475. }
  476. _maxWidth = st::historyReplyPadding.left()
  477. + _maxWidth
  478. + st::historyReplyPadding.right();
  479. _minHeight = st::historyReplyPadding.top()
  480. + st::msgServiceNameFont->height
  481. + optimalTextSize.height()
  482. + st::historyReplyPadding.bottom();
  483. }
  484. int Reply::resizeToWidth(int width) const {
  485. _ripple.animation = nullptr;
  486. const auto previewSkip = _hasPreview
  487. ? (st::messageQuoteStyle.outline
  488. + st::historyReplyPreviewMargin.left()
  489. + st::historyReplyPreview
  490. + st::historyReplyPreviewMargin.right()
  491. - st::historyReplyPadding.left())
  492. : 0;
  493. if (width >= _maxWidth || !_multiline) {
  494. _nameTwoLines = 0;
  495. _expandable = _minHeightExpandable;
  496. _height = _minHeight;
  497. return height();
  498. }
  499. const auto innerw = width
  500. - st::historyReplyPadding.left()
  501. - st::historyReplyPadding.right();
  502. const auto namew = innerw - previewSkip;
  503. const auto desiredNameHeight = _name.countHeight(namew);
  504. _nameTwoLines = (desiredNameHeight > st::semiboldFont->height) ? 1 : 0;
  505. const auto nameh = (_nameTwoLines ? 2 : 1) * st::semiboldFont->height;
  506. const auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;
  507. auto elided = false;
  508. const auto texth = _text.countDimensions(
  509. textGeometry(innerw, firstLineSkip, &elided)).height;
  510. _expandable = elided ? 1 : 0;
  511. _height = st::historyReplyPadding.top()
  512. + nameh
  513. + std::max(texth, st::normalFont->height)
  514. + st::historyReplyPadding.bottom();
  515. return height();
  516. }
  517. Ui::Text::GeometryDescriptor Reply::textGeometry(
  518. int available,
  519. int firstLineSkip,
  520. bool *outElided) const {
  521. return { .layout = [=](int line) {
  522. const auto skip = (line ? 0 : firstLineSkip);
  523. const auto elided = !_multiline
  524. || (!_expanded && (line + 1 >= kNonExpandedLinesLimit));
  525. return Ui::Text::LineGeometry{
  526. .left = skip,
  527. .width = available - skip,
  528. .elided = elided,
  529. };
  530. }, .outElided = outElided };
  531. }
  532. int Reply::height() const {
  533. return _height + st::historyReplyTop + st::historyReplyBottom;
  534. }
  535. QMargins Reply::margins() const {
  536. return QMargins(0, st::historyReplyTop, 0, st::historyReplyBottom);
  537. }
  538. QSize Reply::countMultilineOptimalSize(
  539. int previewSkip) const {
  540. auto elided = false;
  541. const auto max = previewSkip + _text.maxWidth();
  542. const auto result = _text.countDimensions(
  543. textGeometry(max, previewSkip, &elided));
  544. _minHeightExpandable = elided ? 1 : 0;
  545. return {
  546. result.width + st::historyReplyPadding.right(),
  547. std::max(result.height, st::normalFont->height),
  548. };
  549. }
  550. void Reply::paint(
  551. Painter &p,
  552. not_null<const Element*> view,
  553. const Ui::ChatPaintContext &context,
  554. int x,
  555. int y,
  556. int w,
  557. bool inBubble) const {
  558. const auto st = context.st;
  559. const auto stm = context.messageStyle();
  560. y += st::historyReplyTop;
  561. const auto rect = QRect(x, y, w, _height);
  562. const auto selected = context.selected();
  563. const auto backgroundEmojiId = _colorPeer
  564. ? _colorPeer->backgroundEmojiId()
  565. : DocumentId();
  566. const auto colorIndexPlusOne = _colorPeer
  567. ? (_colorPeer->colorIndex() + 1)
  568. : _hiddenSenderColorIndexPlusOne;
  569. const auto useColorIndex = colorIndexPlusOne && !context.outbg;
  570. const auto colorPattern = colorIndexPlusOne
  571. ? st->colorPatternIndex(colorIndexPlusOne - 1)
  572. : 0;
  573. const auto cache = !inBubble
  574. ? (_hasQuoteIcon
  575. ? st->serviceQuoteCache(colorPattern)
  576. : st->serviceReplyCache(colorPattern)).get()
  577. : useColorIndex
  578. ? (_hasQuoteIcon
  579. ? st->coloredQuoteCache(selected, colorIndexPlusOne - 1)
  580. : st->coloredReplyCache(selected, colorIndexPlusOne - 1)).get()
  581. : (_hasQuoteIcon
  582. ? stm->quoteCache[colorPattern]
  583. : stm->replyCache[colorPattern]).get();
  584. const auto &quoteSt = _hasQuoteIcon
  585. ? st::messageTextStyle.blockquote
  586. : st::messageQuoteStyle;
  587. const auto backgroundEmoji = backgroundEmojiId
  588. ? st->backgroundEmojiData(backgroundEmojiId).get()
  589. : nullptr;
  590. const auto backgroundEmojiCache = backgroundEmoji
  591. ? &backgroundEmoji->caches[Ui::BackgroundEmojiData::CacheIndex(
  592. selected,
  593. context.outbg,
  594. inBubble,
  595. colorIndexPlusOne)]
  596. : nullptr;
  597. const auto rippleColor = cache->bg;
  598. if (!inBubble) {
  599. cache->bg = QColor(0, 0, 0, 0);
  600. }
  601. Ui::Text::ValidateQuotePaintCache(*cache, quoteSt);
  602. Ui::Text::FillQuotePaint(p, rect, *cache, quoteSt);
  603. if (backgroundEmoji) {
  604. ValidateBackgroundEmoji(
  605. backgroundEmojiId,
  606. backgroundEmoji,
  607. backgroundEmojiCache,
  608. cache,
  609. view);
  610. if (!backgroundEmojiCache->frames[0].isNull()) {
  611. FillBackgroundEmoji(p, rect, _hasQuoteIcon, *backgroundEmojiCache);
  612. }
  613. }
  614. if (!inBubble) {
  615. cache->bg = rippleColor;
  616. }
  617. if (_ripple.animation) {
  618. _ripple.animation->paint(p, x, y, w, &rippleColor);
  619. if (_ripple.animation->empty()) {
  620. _ripple.animation.reset();
  621. }
  622. }
  623. auto hasPreview = (_hasPreview != 0);
  624. auto previewSkip = hasPreview
  625. ? (st::messageQuoteStyle.outline
  626. + st::historyReplyPreviewMargin.left()
  627. + st::historyReplyPreview
  628. + st::historyReplyPreviewMargin.right()
  629. - st::historyReplyPadding.left())
  630. : 0;
  631. if (hasPreview && w <= st::historyReplyPadding.left() + previewSkip) {
  632. hasPreview = false;
  633. previewSkip = 0;
  634. }
  635. const auto pausedSpoiler = context.paused
  636. || On(PowerSaving::kChatSpoiler);
  637. auto textLeft = x + st::historyReplyPadding.left();
  638. auto textTop = y
  639. + st::historyReplyPadding.top()
  640. + (st::msgServiceNameFont->height * (_nameTwoLines ? 2 : 1));
  641. if (w > st::historyReplyPadding.left()) {
  642. if (_displaying) {
  643. if (hasPreview) {
  644. const auto data = view->data()->Get<HistoryMessageReply>();
  645. const auto message = data
  646. ? data->resolvedMessage.get()
  647. : nullptr;
  648. const auto media = message ? message->media() : nullptr;
  649. const auto image = media
  650. ? media->replyPreview()
  651. : !data
  652. ? nullptr
  653. : data->resolvedStory
  654. ? data->resolvedStory->replyPreview()
  655. : data->fields().externalMedia
  656. ? data->fields().externalMedia->replyPreview()
  657. : nullptr;
  658. if (image) {
  659. auto to = style::rtlrect(
  660. x + st::historyReplyPreviewMargin.left(),
  661. y + st::historyReplyPreviewMargin.top(),
  662. st::historyReplyPreview,
  663. st::historyReplyPreview,
  664. w + 2 * x);
  665. const auto preview = image->pixSingle(
  666. image->size() / style::DevicePixelRatio(),
  667. {
  668. .colored = (context.selected()
  669. ? &st->msgStickerOverlay()
  670. : nullptr),
  671. .options = Images::Option::RoundSmall,
  672. .outer = to.size(),
  673. });
  674. p.drawPixmap(to.x(), to.y(), preview);
  675. if (_spoiler) {
  676. view->clearCustomEmojiRepaint();
  677. Ui::FillSpoilerRect(
  678. p,
  679. to,
  680. Ui::DefaultImageSpoiler().frame(
  681. _spoiler->index(
  682. context.now,
  683. pausedSpoiler)));
  684. }
  685. }
  686. }
  687. const auto textw = w
  688. - st::historyReplyPadding.left()
  689. - st::historyReplyPadding.right();
  690. const auto namew = textw
  691. - previewSkip
  692. - (_hasQuoteIcon
  693. ? st::messageTextStyle.blockquote.icon.width()
  694. : 0);
  695. auto firstLineSkip = _nameTwoLines ? 0 : previewSkip;
  696. if (namew > 0) {
  697. p.setPen(!inBubble
  698. ? st->msgImgReplyBarColor()->c
  699. : useColorIndex
  700. ? FromNameFg(context, colorIndexPlusOne - 1)
  701. : stm->msgServiceFg->c);
  702. _name.drawLeftElided(
  703. p,
  704. x + st::historyReplyPadding.left() + previewSkip,
  705. y + st::historyReplyPadding.top(),
  706. namew,
  707. w + 2 * x,
  708. _nameTwoLines ? 2 : 1);
  709. p.setPen(inBubble
  710. ? stm->historyTextFg
  711. : st->msgImgReplyBarColor());
  712. view->prepareCustomEmojiPaint(p, context, _text);
  713. auto replyToTextPalette = &(!inBubble
  714. ? st->imgReplyTextPalette()
  715. : useColorIndex
  716. ? st->coloredTextPalette(selected, colorIndexPlusOne - 1)
  717. : stm->replyTextPalette);
  718. auto owned = std::optional<style::owned_color>();
  719. auto copy = std::optional<style::TextPalette>();
  720. if (inBubble && colorIndexPlusOne) {
  721. copy.emplace(*replyToTextPalette);
  722. owned.emplace(cache->icon);
  723. copy->linkFg = owned->color();
  724. replyToTextPalette = &*copy;
  725. }
  726. if (_replyToStory) {
  727. st::dialogsMiniReplyStory.icon.icon.paint(
  728. p,
  729. textLeft + firstLineSkip,
  730. textTop,
  731. w + 2 * x,
  732. replyToTextPalette->linkFg->c);
  733. firstLineSkip += st::dialogsMiniReplyStory.skipText
  734. + st::dialogsMiniReplyStory.icon.icon.width();
  735. }
  736. _text.draw(p, {
  737. .position = { textLeft, textTop },
  738. .geometry = textGeometry(textw, firstLineSkip),
  739. .palette = replyToTextPalette,
  740. .spoiler = Ui::Text::DefaultSpoilerCache(),
  741. .now = context.now,
  742. .pausedEmoji = (context.paused
  743. || On(PowerSaving::kEmojiChat)),
  744. .pausedSpoiler = pausedSpoiler,
  745. .elisionLines = 1,
  746. });
  747. p.setTextPalette(stm->textPalette);
  748. }
  749. } else {
  750. p.setFont(st::msgDateFont);
  751. p.setPen(cache->icon);
  752. p.drawTextLeft(
  753. textLeft,
  754. (y
  755. + st::historyReplyPadding.top()
  756. + (st::msgDateFont->height / 2)),
  757. w + 2 * x,
  758. st::msgDateFont->elided(
  759. _stateText,
  760. x + w - textLeft - st::historyReplyPadding.right()));
  761. }
  762. }
  763. }
  764. void Reply::createRippleAnimation(
  765. not_null<const Element*> view,
  766. QSize size) {
  767. _ripple.animation = std::make_unique<Ui::RippleAnimation>(
  768. st::defaultRippleAnimation,
  769. Ui::RippleAnimation::RoundRectMask(
  770. size,
  771. st::messageQuoteStyle.radius),
  772. [=] { view->repaint(); });
  773. }
  774. void Reply::saveRipplePoint(QPoint point) const {
  775. _ripple.lastPoint = point;
  776. }
  777. void Reply::addRipple() {
  778. if (_ripple.animation) {
  779. _ripple.animation->add(_ripple.lastPoint);
  780. }
  781. }
  782. void Reply::stopLastRipple() {
  783. if (_ripple.animation) {
  784. _ripple.animation->lastStop();
  785. }
  786. }
  787. TextWithEntities Reply::PeerEmoji(
  788. not_null<History*> history,
  789. PeerData *peer) {
  790. return PeerEmoji(&history->owner(), peer);
  791. }
  792. TextWithEntities Reply::PeerEmoji(
  793. not_null<Data::Session*> owner,
  794. PeerData *peer) {
  795. using namespace std;
  796. const auto icon = !peer
  797. ? pair(&st::historyReplyUser, st::historyReplyUserPadding)
  798. : peer->isBroadcast()
  799. ? pair(&st::historyReplyChannel, st::historyReplyChannelPadding)
  800. : (peer->isChannel() || peer->isChat())
  801. ? pair(&st::historyReplyGroup, st::historyReplyGroupPadding)
  802. : pair(&st::historyReplyUser, st::historyReplyUserPadding);
  803. return Ui::Text::SingleCustomEmoji(
  804. owner->customEmojiManager().registerInternalEmoji(
  805. *icon.first,
  806. icon.second));
  807. }
  808. TextWithEntities Reply::ForwardEmoji(not_null<Data::Session*> owner) {
  809. return Ui::Text::SingleCustomEmoji(
  810. owner->customEmojiManager().registerInternalEmoji(
  811. st::historyReplyForward,
  812. st::historyReplyForwardPadding));
  813. }
  814. TextWithEntities Reply::ComposePreviewName(
  815. not_null<History*> history,
  816. not_null<HistoryItem*> to,
  817. bool quote) {
  818. const auto sender = [&] {
  819. if (const auto from = to->displayFrom()) {
  820. return not_null(from);
  821. }
  822. return to->author();
  823. }();
  824. const auto toPeer = to->history()->peer;
  825. const auto displayAsExternal = (to->history() != history);
  826. const auto groupNameAdded = displayAsExternal
  827. && (toPeer != sender)
  828. && (toPeer->isChat() || toPeer->isMegagroup());
  829. const auto shorten = groupNameAdded || quote;
  830. auto nameFull = TextWithEntities();
  831. using namespace HistoryView;
  832. if (displayAsExternal && !groupNameAdded) {
  833. nameFull.append(Reply::PeerEmoji(history, sender));
  834. }
  835. nameFull.append(shorten ? sender->shortName() : sender->name());
  836. if (groupNameAdded) {
  837. nameFull.append(' ').append(Reply::PeerEmoji(history, toPeer));
  838. nameFull.append(toPeer->name());
  839. }
  840. return (quote
  841. ? tr::lng_preview_reply_to_quote
  842. : tr::lng_preview_reply_to)(
  843. tr::now,
  844. lt_name,
  845. nameFull,
  846. Ui::Text::WithEntities);
  847. }
  848. void Reply::unloadPersistentAnimation() {
  849. _text.unloadPersistentAnimation();
  850. }
  851. } // namespace HistoryView